Compare commits

...
Author SHA1 Message Date
Diego ImbertandClaude Opus 5 b2596c6adc fix: default Path's owner controls to the user acting in its workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:08:43 +02:00
Diego ImbertandClaude Opus 5 43ed8c6d56 fix: keep session permissions reactive to the acting role and restore tabs as last seen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:37:25 +02:00
Diego ImbertandClaude Opus 5 f4e8fe4838 fix: judge every session-editor permission by the user acting in that workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:21:56 +02:00
Diego Imbert dbe0b49aca Merge remote-tracking branch 'origin/main' into remove-iframe-editors
# Conflicts:
#	frontend/src/lib/components/AIAgentLogViewer.svelte
#	frontend/src/lib/components/ResourceForm.svelte
#	frontend/src/lib/components/raw_apps/RawAppEditor.svelte
#	frontend/src/lib/components/settings/CreateToken.svelte
2026-09-18 15:06:51 +02:00
Diego ImbertandClaude Opus 5 f5d4fdab7d fix: judge trigger permissions by the user acting in the session workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 15:03:02 +02:00
Diego Imbert fffd8b5217 Merge remote-tracking branch 'origin/main' into remove-iframe-editors
# Conflicts:
#	frontend/src/lib/components/flows/conversations/FlowChat.svelte
2026-09-18 14:10:40 +02:00
Diego ImbertandClaude Opus 5 83778f571f feat: close a page item tab from its editor header
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:06:57 +02:00
Diego ImbertandClaude Opus 5 6fb48c64df fix: follow a saved page item in the tab's own workspace and restore in-frame rows
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 17:49:39 +02:00
Diego ImbertandClaude Opus 5 09ab9b8e6a fix: show a page item tab whose item is gone instead of mounting its editor
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 17:33:44 +02:00
Diego Imbert ca44af5f5a Merge remote-tracking branch 'origin/main' into remove-iframe-editors 2026-09-17 17:05:16 +02:00
Diego ImbertandClaude Opus 5 a4eb80bd8d fix: guard every component under a session editor against the navigation workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 17:05:15 +02:00
Diego ImbertandClaude Opus 5 34aecb0d29 fix: resolve open in workspace for every preview tab kind in one place
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:49:41 +02:00
Diego ImbertandClaude Opus 5 cc84d07484 fix: reload page item tabs only in sessions acting on the tool's workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:49:41 +02:00
Diego ImbertandClaude Opus 5 acbdb285c9 refactor: one operating-workspace context for editors acting on a session's workspace
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:45:19 +02:00
Diego ImbertandClaude Opus 5 29abd63de6 fix: take a trigger's flow kind from its config and name the written trigger
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:50:04 +02:00
Diego ImbertandClaude Opus 5 9d029c0d44 feat: edit variables, resources and triggers in their own session tab
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:39:22 +02:00
256 changed files with 3455 additions and 1774 deletions
@@ -3,11 +3,13 @@
import Select from './select/Select.svelte'
import { fetchAvailableModels, AI_PROVIDERS } from './copilot/lib'
import type { AIProvider, ProviderConfig } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import ResourcePicker from './ResourcePicker.svelte'
import Toggle from './Toggle.svelte'
import { saveConfig, removeConfig, isSameAsStoredConfig } from './aiProviderStorage'
import AIReasoningEffortPicker from './AIReasoningEffortPicker.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
value: ProviderConfig | undefined
@@ -26,7 +28,7 @@
workspace = undefined
}: Props = $props()
let effectiveWorkspace = $derived(workspace ?? $workspaceStore ?? '')
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace ?? '')
let value = $derived.by(() => {
if (!_uncheckedValue || typeof _uncheckedValue !== 'object') return undefined
@@ -2,7 +2,6 @@
import { ExternalLink, Globe } from 'lucide-svelte'
import { JobService, type Job } from '$lib/gen'
import { base } from '$lib/base'
import { workspaceStore } from '$lib/stores'
import { msToReadableTimeShort } from '$lib/utils'
import ChatCollapsibleCard from './copilot/chat/ChatCollapsibleCard.svelte'
import ToolContentDisplay from './copilot/chat/ToolContentDisplay.svelte'
@@ -10,6 +9,9 @@
import GfmMarkdown from './GfmMarkdown.svelte'
import type { AgentTraceEntry } from './agentTrace'
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
entries: AgentTraceEntry[]
@@ -40,7 +42,7 @@
try {
jobs.set(
jobId,
await JobService.getJob({ id: jobId, workspace: workspaceId ?? $workspaceStore! })
await JobService.getJob({ id: jobId, workspace: workspaceId ?? $operatingWorkspace! })
)
} catch {
// A tool job can be gone (retention) or unreadable. The row still has its
@@ -121,7 +123,7 @@
{:else if entry.jobId}
<a
class="text-2xs text-accent inline-flex items-center gap-1 w-fit hover:underline"
href="{base}/run/{entry.jobId}?workspace={workspaceId ?? $workspaceStore}"
href="{base}/run/{entry.jobId}?workspace={workspaceId ?? $operatingWorkspace}"
target="_blank"
rel="noreferrer"
>
@@ -1,7 +1,6 @@
<script lang="ts">
import { OauthService, type ResourceType } from '$lib/gen'
import FilesetEditor from './FilesetEditor.svelte'
import { workspaceStore } from '$lib/stores'
import { emptySchema, emptyString } from '$lib/utils'
import SchemaForm from './SchemaForm.svelte'
import Toggle from './Toggle.svelte'
@@ -20,6 +19,9 @@
import { base } from '$lib/base'
import { isDataTableWizardEnabled } from './workspaceSettings/utils.svelte'
import { parsePostgresConnectionString } from '$lib/utils/postgresConnectionString'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
resourceType: string
@@ -152,7 +154,7 @@
}
}
$effect(() => {
$workspaceStore && untrack(() => loadSchema())
$operatingWorkspace && untrack(() => loadSchema())
})
$effect(() => {
notFound && rawCode && untrack(() => parseJson())
@@ -1,7 +1,7 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import { userStore } from '$lib/stores'
import LabelsInput from './LabelsInput.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import {
@@ -53,6 +53,9 @@
import Label from './Label.svelte'
import ResourcePathHint from './ResourcePathHint.svelte'
import SchemaForm from './SchemaForm.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
step?: number
@@ -84,7 +87,7 @@
fillPath = undefined
}: Props = $props()
let effectiveWorkspace = $derived(workspace ?? $workspaceStore!)
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace!)
let isValid = $state(true)
+5 -3
View File
@@ -1,12 +1,14 @@
<script lang="ts">
import { ResourceService, VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { copyToClipboard, truncate } from '$lib/utils'
import { ClipboardCopy, Expand } from 'lucide-svelte'
import Drawer from './common/drawer/Drawer.svelte'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import Tooltip from './Tooltip.svelte'
import { Button, DrawerContent } from './common'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
value: any
@@ -22,14 +24,14 @@
async function getResource(path: string) {
jsonViewerContent = await ResourceService.getResourceValue({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
path
})
}
async function getVariable(path: string) {
jsonViewerContent = await VariableService.getVariableValue({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
path
})
}
+4 -2
View File
@@ -41,12 +41,14 @@
import { safeSelectItems } from './select/utils.svelte'
import S3ArgInput from './common/fileUpload/S3ArgInput.svelte'
import { base } from '$lib/base'
import { workspaceStore } from '$lib/stores'
import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte'
import AIProviderPicker from './AIProviderPicker.svelte'
import TextInput from './text_input/TextInput.svelte'
import FileInput from './common/fileInput/FileInput.svelte'
import { randomUUID } from '$lib/utils/uuid'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
label?: string
@@ -823,7 +825,7 @@
/>
{/await}
{:else if inputCat == 'object' && format?.startsWith('jsonschema-')}
{#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $workspaceStore ?? '')}
{#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $operatingWorkspace ?? '')}
<Loader2 class="animate-spin" />
{:then schema}
{#if !schema || !schema.properties}
@@ -1,9 +1,11 @@
<script lang="ts">
import { JobService, type FlowValue } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { tryEvery } from '$lib/utils'
import { Check, LoaderCircle, Server, X, Cpu } from 'lucide-svelte'
import Button from './common/button/Button.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface CredentialsCheckResult {
available: boolean
@@ -28,7 +30,7 @@
apiResult = null
try {
const response = await fetch(`/api/w/${$workspaceStore}/ai/check_bedrock_credentials`)
const response = await fetch(`/api/w/${$operatingWorkspace}/ai/check_bedrock_credentials`)
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
@@ -78,7 +80,7 @@
}
const job = await JobService.runFlowPreview({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
requestBody: {
value: flowValue as unknown as FlowValue,
args: {}
@@ -88,7 +90,7 @@
tryEvery({
tryCode: async () => {
const testResult = await JobService.getCompletedJob({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
id: job
})
@@ -130,7 +132,7 @@
workerStatus = 'error'
try {
await JobService.cancelQueuedJob({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
id: job,
requestBody: {
reason: 'Timeout checking Bedrock credentials'
@@ -1,9 +1,11 @@
<script lang="ts">
import Select from './select/Select.svelte'
import { WorkspaceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { RefreshCcw } from 'lucide-svelte'
import { Button } from './common'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface ChannelItem {
channel_id?: string
@@ -21,8 +23,8 @@
showRefreshButton?: boolean
onError?: (error: Error) => void
onSelectedChannelChange?: (channel: ChannelItem | undefined) => void
/** Workspace to list Teams channels from; defaults to the nav
* `$workspaceStore`. A forked session passes its acting workspace. */
/** Workspace to list Teams channels from; defaults to the operating workspace (see
* `useOperatingWorkspace`). */
workspace?: string
}
@@ -40,7 +42,7 @@
workspace = undefined
}: Props = $props()
let effectiveWorkspace = $derived(workspace ?? $workspaceStore)
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace)
let isFetching = $state(false)
let loadedChannels = $state<ChannelItem[]>([])
@@ -1,7 +1,9 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import ClipboardPanel from './details/ClipboardPanel.svelte'
import Section from './Section.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
let url = $derived(`${window.location.protocol}//${window.location.hostname}/`)
</script>
@@ -20,7 +22,7 @@
<span class="font-medium">Setup the wmill cli for this workspace & remote:</span>
<div class="mt-1">
<ClipboardPanel
content={`wmill workspace add ${$workspaceStore} ${$workspaceStore} ${url}`}
content={`wmill workspace add ${$operatingWorkspace} ${$operatingWorkspace} ${url}`}
/>
</div>
</li>
+6 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { superadmin, userStore, type DBSchema } from '$lib/stores'
import { superadmin, type DBSchema } from '$lib/stores'
import {
ChevronDownIcon,
EditIcon,
@@ -33,6 +33,10 @@
import type { DbFeatures } from './apps/components/display/dbtable/dbFeatures'
import Star from './Star.svelte'
import type { Asset } from '$lib/gen'
import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte'
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
/** Represents a selected table with its schema */
export interface SelectedTable {
@@ -644,7 +648,7 @@
Import schema from database
</span>
</button>
{#if !!$userStore?.is_admin || !!$superadmin}
{#if !!actingUser?.is_admin || !!$superadmin}
<button
onclick={() => onImport('schema_and_data')}
class="hover:opacity-70 transition-opacity rounded-md border aspect-square w-52 gap-4 p-4 center-center flex-col"
@@ -1,5 +1,5 @@
<script lang="ts">
import { dbSchemas, workspaceStore, type DBSchema } from '$lib/stores'
import { dbSchemas, type DBSchema } from '$lib/stores'
import { sortArray } from '$lib/utils'
import { Loader2, RefreshCcw } from 'lucide-svelte'
import Alert from './common/alert/Alert.svelte'
@@ -29,6 +29,9 @@
import { createAsyncConfirmationModal } from './common/confirmationModal/asyncConfirmationModal.svelte'
import Portal from '$lib/components/Portal.svelte'
import { outOfOrderRunMessage } from './workspaceSettings/datatableMigrationUtils'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
input?: DbInput
@@ -44,9 +47,8 @@
/** Tables that are already added and should show as disabled */
disabledTables?: SelectedTable[]
onImport?: (mode: 'schema_and_data' | 'schema_only') => void
/** Workspace the datatable/schema lookups run against. Defaults to the
* navigation `$workspaceStore`; pass the acting workspace when embedded in
* a session preview whose workspace differs from the top nav. */
/** Workspace the datatable/schema lookups run against. Defaults to the operating
* workspace (see `useOperatingWorkspace`). */
workspace?: string
/** Worker tag every job of this manager runs on, overriding the database
* language's native tag. Bound so the hints below can offer to set it. */
@@ -68,7 +70,7 @@
workerTag = $bindable()
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[schemaCacheKey(input)])
+8 -6
View File
@@ -66,7 +66,6 @@
</script>
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { createGrid, type GridApi, type IDatasource } from 'ag-grid-community'
import { transformColumnDefs } from './apps/components/display/table/utils'
@@ -82,6 +81,9 @@
import 'ag-grid-community/styles/ag-theme-alpine.css'
import '$lib/components/apps/components/display/table/theme/windmill-theme.css'
import { untrack } from 'svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
type Props = {
dbTableOps: IDbTableOps
@@ -101,7 +103,7 @@
let datasource: IDatasource = {
getRows: async function (params) {
if (!$workspaceStore) return params.failCallback()
if (!$operatingWorkspace) return params.failCallback()
let lastRow = rowCount && rowCount <= params.endRow ? rowCount : -1
const items = await dbTableOps.getRows({
@@ -132,7 +134,7 @@
minWidth: 150,
editable: true,
onCellValueChanged: (e) => {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
const colDef = e.colDef as unknown as { field: string; datatype: string }
dbTableOps
.onUpdate?.(
@@ -169,7 +171,7 @@
let prevUpdateKey: any = undefined
$effect(() => {
if (!$workspaceStore || !api) return
if (!$operatingWorkspace || !api) return
const key = { quicksearch, colDefs: dbTableOps.colDefs, refreshCount, rowFilter }
if (deepEqual(key, prevUpdateKey)) return
prevUpdateKey = key
@@ -191,7 +193,7 @@
),
...(dbTableOps.onDelete && {
onDelete: (values) => {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
dbTableOps
.onDelete?.({ values })
.then(() => {
@@ -249,7 +251,7 @@
columnDefs={dbTableOps.colDefs ?? []}
dbType={dbTableOps.dbType}
onInsert={(values) => {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
dbTableOps.onInsert?.({ values }).then((result) => {
refresh?.()
sendUserToast('Row inserted')
@@ -1,10 +1,12 @@
<script lang="ts">
import { WorkspaceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Select from './select/Select.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
value?: string | undefined
@@ -29,7 +31,7 @@
}: Props = $props()
let datatables = usePromise(() =>
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' }).then((d) =>
WorkspaceService.listDataTables({ workspace: $operatingWorkspace ?? '' }).then((d) =>
d.map((d) => d.name)
)
)
@@ -1,10 +1,13 @@
<script lang="ts">
import { userStore } from '$lib/stores'
import { SettingsIcon } from 'lucide-svelte'
import { Button } from './common'
import Drawer from './common/drawer/Drawer.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import DefaultScriptsInner from './DefaultScriptsInner.svelte'
import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte'
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
placement?: 'left' | 'right'
@@ -17,7 +20,7 @@
let drawer: Drawer | undefined = $state()
</script>
{#if $userStore?.is_admin || $userStore?.is_super_admin}
{#if actingUser?.is_admin || actingUser?.is_super_admin}
<Drawer bind:this={drawer} {placement}>
<DrawerContent title="Edit Default Scripts" on:close={drawer?.closeDrawer}>
<DefaultScriptsInner />
@@ -1,10 +1,13 @@
<script lang="ts">
import { WorkspaceService, type Script, type WorkspaceDefaultScripts } from '$lib/gen'
import { defaultScripts, workspaceStore } from '$lib/stores'
import { defaultScripts } from '$lib/stores'
import { flip } from 'svelte/animate'
import Toggle from './Toggle.svelte'
import { defaultScriptLanguages } from '$lib/scripts'
import Alert from './common/alert/Alert.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
small?: boolean
@@ -29,7 +32,7 @@
}
defaultScripts.update((s) => ({ ...s, order: norder }))
await WorkspaceService.editDefaultScripts({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
requestBody: $defaultScripts
})
}
@@ -1,10 +1,12 @@
<script lang="ts">
import { WorkspaceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Select from './select/Select.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
value?: string | undefined
@@ -29,9 +31,8 @@
}: Props = $props()
let ducklakes = usePromise(() =>
WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
WorkspaceService.listDucklakes({ workspace: $operatingWorkspace ?? '' })
)
</script>
<div class={className}>
@@ -46,9 +47,6 @@
{onClear}
/>
{#if showSchemaExplorer && value && assetCanBeExplored({ kind: 'ducklake', path: value })}
<ExploreAssetButton
class="mt-1 w-fit"
asset={{ kind: 'ducklake', path: value }}
/>
<ExploreAssetButton class="mt-1 w-fit" asset={{ kind: 'ducklake', path: value }} />
{/if}
</div>
@@ -4,7 +4,6 @@
const bubble = createBubbler()
import type { Schema } from '$lib/common'
import { VariableService, type ScriptLang } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Button } from './common'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
@@ -36,6 +35,9 @@
import Section from '$lib/components/Section.svelte'
import Editor from './Editor.svelte'
import AddPropertyV2 from './schema/AddPropertyV2.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
// export let openEditTab: () => void = () => {}
const dispatch = createEventDispatcher()
@@ -128,7 +130,7 @@
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
$effect.pre(() => {
if (args == undefined) {
+12 -10
View File
@@ -42,7 +42,6 @@
import { editorConfig, registerWebviewPaste, updateOptions } from '$lib/editorUtils'
import { editorFontSize } from '$lib/editorFontSize.svelte'
import { createHash as randomHash } from '$lib/editorLangUtils'
import { workspaceStore } from '$lib/stores'
import DdlMigrationGuard from './DdlMigrationGuard.svelte'
import {
type Preview,
@@ -120,6 +119,9 @@
import { rawAppLintStore, type MonacoLintError } from './raw_apps/lintStore'
import { MarkerSeverity } from 'monaco-editor'
import { resource, useDebounce, watch } from 'runed'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
// import EditorTheme from './EditorTheme.svelte'
let divEl: HTMLDivElement | null = $state(null)
@@ -684,7 +686,7 @@
// via a short-TTL cache — macros are late-bound, so mild staleness is fine.
async function addWorkspaceMacroCompletions() {
workspaceMacroCompletor?.dispose()
const workspace = $workspaceStore
const workspace = $operatingWorkspace
if (!workspace) return
let macros: Awaited<ReturnType<typeof listWorkspaceMacrosCached>> = []
try {
@@ -739,7 +741,7 @@
provideCompletionItems: async function (model, position) {
// Read the store per request, not at registration — the provider
// outlives a workspace switch.
const workspace = $workspaceStore
const workspace = $operatingWorkspace
if (!workspace) return { suggestions: [] }
const before = model.getLineContent(position.lineNumber).slice(0, position.column - 1)
if (!/^\s*(\/\/|--|#)\s*(column|data_test|on|materialize)\b/.test(before)) {
@@ -780,7 +782,7 @@
$dbSchemas[resourcePath] = await getDbSchemas(
lang === 'graphql' ? 'graphql' : (scriptLang ?? ''),
resourcePath,
$workspaceStore,
$operatingWorkspace,
(e) => console.error(`error getting ${lang} (${scriptLang}) db schema`, e),
{ customTag }
)
@@ -1778,9 +1780,9 @@
let customTsTypesData = resource([() => lang], async () => {
if (lang !== 'typescript') return undefined
let datatables = (
await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
await WorkspaceService.listDataTables({ workspace: $operatingWorkspace ?? '' })
).map((d) => d.name)
let ducklakes = await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
let ducklakes = await WorkspaceService.listDucklakes({ workspace: $operatingWorkspace ?? '' })
return { datatables, ducklakes }
})
function setTypescriptCustomTypes() {
@@ -1822,7 +1824,7 @@
scriptLang === 'nativets')
) {
const resourceTypes = await ResourceService.listResourceType({
workspace: $workspaceStore ?? ''
workspace: $operatingWorkspace ?? ''
})
const namespace = formatResourceTypes(
@@ -2023,7 +2025,7 @@
$lspTokenStore = newToken
token = newToken
}
let root = hostname + '/api/scripts_u/tokened_raw/' + $workspaceStore + '/' + token
let root = hostname + '/api/scripts_u/tokened_raw/' + $operatingWorkspace + '/' + token
return root
}
@@ -2274,10 +2276,10 @@
<svelte:window onkeydown={onKeyDown} />
<EditorTheme />
{#if datatableForMigrations && $workspaceStore}
{#if datatableForMigrations && $operatingWorkspace}
<DdlMigrationGuard
bind:this={ddlGuard}
workspace={$workspaceStore}
workspace={$operatingWorkspace}
datatable={datatableForMigrations}
/>
{/if}
+6 -5
View File
@@ -22,7 +22,6 @@
<script lang="ts">
import { ResourceService, VariableService, WorkspaceService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import type Editor from './Editor.svelte'
import ItemPicker from './ItemPicker.svelte'
@@ -76,6 +75,9 @@
import FlowInlineScriptAiButton from './copilot/FlowInlineScriptAIButton.svelte'
import GitRepoPopoverPicker from './GitRepoPopoverPicker.svelte'
import { insertDelegateToGitRepoInCode } from '$lib/ansibleUtils'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
lang: SupportedLanguage | 'bunnative' | undefined
@@ -121,9 +123,8 @@
right?: import('svelte').Snippet
openAiChat?: boolean
moduleId?: string
// Workspace to scope variable/resource/data-table lookups to. Defaults to
// the nav `$workspaceStore`; an AI-session live editor passes the session's
// acting workspace (a fork) so the helper pickers hit the right workspace.
// Workspace to scope variable/resource/data-table lookups to. Defaults to the
// operating workspace (see `useOperatingWorkspace`).
workspace?: string
}
@@ -153,7 +154,7 @@
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let contextualVariablePicker: ItemPicker | undefined = $state()
let variablePicker: ItemPicker | undefined = $state()
@@ -15,7 +15,15 @@
} from '$lib/components/workspacePicker'
import BreadcrumbSegment from '$lib/components/BreadcrumbSegment.svelte'
import { isOwner } from '$lib/utils'
import { userStore, workspaceStore } from '$lib/stores'
import { userStore } from '$lib/stores'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
summary?: string
@@ -47,9 +55,8 @@
* dropped, leaving only the summary. Used by the condensed session-
* preview top bar to save vertical room. */
hidePath?: boolean
/** Workspace whose items the breadcrumb picker lists. Session live
* editors pass their acting workspace so the picker isn't scoped to the
* navigation workspace; falls back to $workspaceStore in the picker. */
/** Workspace whose items the breadcrumb picker lists; defaults to the operating
* workspace (see `useOperatingWorkspace`). */
workspaceId?: string
}
@@ -116,7 +123,7 @@
// Treat an empty path as ownable so the pen popover lets a user pick the
// path for a brand-new item. `Path.reset()` then synthesizes a default
// under their own user/folder scope.
let own = $derived(!path || isOwner(path, $userStore, $workspaceStore))
let own = $derived(!path || isOwner(path, actingUser, $operatingWorkspace))
// Virtual entry for the picker: surfaces the currently-edited item at its
// live path (which may differ from `savedPath` mid-rename, so the picker
@@ -31,7 +31,7 @@
import type { Schema, SupportedLanguage } from '$lib/common'
import { base } from '$lib/base'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { enterpriseLicense } from '$lib/stores'
import MsTeamsIcon from '$lib/components/icons/MSTeamsIcon.svelte'
import { classNames, emptySchema, emptyString, sendUserToast, tryEvery } from '$lib/utils'
import MultiSelect from '$lib/components/select/MultiSelect.svelte'
@@ -61,6 +61,9 @@
import SmtpConfigurationStatus from './common/smtp/SmtpConfigurationStatus.svelte'
import { SettingService } from '$lib/gen'
import { isSmtpSettingsValid } from './instanceSettings/SmtpSettings.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const slackRecoveryHandler = hubPaths.slackRecoveryHandler
const slackHandlerScriptPath = hubPaths.slackErrorHandler
@@ -81,9 +84,8 @@
customHandlerKind?: 'flow' | 'script'
customTabTooltip?: import('svelte').Snippet
noMargin?: boolean
/** Workspace for handler lookup / settings / test jobs. Defaults to the
* nav `$workspaceStore`; a trigger editor in a forked session passes its
* acting workspace so the handler is resolved and saved there. */
/** Workspace for handler lookup / settings / test jobs. Defaults to the operating
* workspace (see `useOperatingWorkspace`). */
workspace?: string
/** Offer the instance critical alert channels as a destination. Workspace-level
* error handling only: schedules and triggers have no such setting. */
@@ -106,13 +108,12 @@
showInstanceAlerts = false
}: Props = $props()
let effectiveWorkspace = $derived(workspace ?? $workspaceStore)
// Carry the acting workspace onto the "create from template" route when an
// explicit override is set, so a forked session creates the handler script
// there. `customScriptTemplate` already has a query string (`?hub=…`).
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace)
// Carry the workspace onto the "create from template" route, so the handler script is
// created where this handler is saved. `customScriptTemplate` already has a query string.
let templateHref = $derived(
workspace
? `${customScriptTemplate}&workspace=${encodeURIComponent(workspace)}`
effectiveWorkspace
? `${customScriptTemplate}&workspace=${encodeURIComponent(effectiveWorkspace)}`
: customScriptTemplate
)
@@ -33,15 +33,18 @@
import { Button, ButtonType } from '$lib/components/common'
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import { VolumeService } from '$lib/gen'
import {
globalDbManagerDrawer,
globalS3FilePickerExplorer,
userStore,
workspaceStore
} from '$lib/stores'
import { globalDbManagerDrawer, globalS3FilePickerExplorer } from '$lib/stores'
import { isS3Uri } from '$lib/utils'
import { Database, File, HardDriveIcon } from 'lucide-svelte'
import DucklakeIcon from './icons/DucklakeIcon.svelte'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
const {
asset,
@@ -69,7 +72,7 @@
} = $props()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
const assetUri = $derived(formatAsset(asset))
// Contexts with a select/upload flow pass their own picker; everything else
// (e.g. the resources list) falls back to the global read-only explorer.
@@ -80,7 +83,7 @@
</script>
<Button
disabled={$userStore?.operator || disabled}
disabled={actingUser?.operator || disabled}
unifiedSize={'md'}
variant={buttonVariant}
wrapperClasses={className}
+14 -12
View File
@@ -17,13 +17,7 @@
linkedAgentToolsVersion,
migrateLinkedAgentToolsScope
} from '$lib/components/flows/linkedAgentToolsStore.svelte'
import {
enterpriseLicense,
userStore,
userWorkspaces,
workspaceStore,
usedTriggerKinds
} from '$lib/stores'
import { enterpriseLicense, userStore, userWorkspaces, usedTriggerKinds } from '$lib/stores'
import {
generateRandomString,
orderedJsonStringify,
@@ -111,6 +105,14 @@
import { UserDraft } from '$lib/userDraft.svelte'
import { setOpenInSessionHandoff } from './sessions/openInSessionContext'
import { getEditorStoragePath, setEditorStoragePath } from './editorStoragePathContext'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
let {
initialPath = $bindable(''),
@@ -158,9 +160,9 @@
// and the AutosaveIndicator all target it. Falls back to the global store, so
// the full-page editor is unchanged; the sessions preview overrides it to the
// session's (forked) workspace, so an embedded editor acts on the session's
// fork rather than the navigation workspace ($workspaceStore, which stays put).
// fork rather than the navigation workspace (`workspaceStore`, which stays put).
// indicatorPath is the matching draft path.
const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore)
const opWorkspace = $derived(autosaveWorkspace ?? $operatingWorkspace)
const indicatorPath = $derived(autosavePath ?? liveEditorDraftStoragePath)
let initialPathStore = writable(initialPath)
@@ -607,7 +609,7 @@
await deployTriggers(
triggersToDeploy,
opWorkspace,
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
!!actingUser?.is_admin || !!actingUser?.is_super_admin,
usedTriggerKinds,
$pathStore,
true
@@ -618,7 +620,7 @@
await deployTriggers(
triggersToDeploy,
opWorkspace,
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
!!actingUser?.is_admin || !!actingUser?.is_super_admin,
usedTriggerKinds,
initialPath
)
@@ -1394,7 +1396,7 @@
<AIChangesWarningModal bind:open={aiChangesWarningOpen} onConfirm={aiChangesConfirmCallback} />
{#key renderCount}
{#if !$userStore?.operator}
{#if !actingUser?.operator}
{#if $pathStore}
<FlowHistory bind:this={flowHistory} path={$pathStore} {onHistoryRestore} />
{/if}
@@ -10,10 +10,12 @@
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
import { dfs } from './flows/dfs'
import { workspaceStore } from '$lib/stores'
import { untrack } from 'svelte'
import { publishLinkedAgentTools } from './flows/flowState'
import { linkedToolsScope } from './flows/linkedAgentToolsStore.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
flow: {
@@ -45,7 +47,7 @@
noGraph = false,
triggerNode = false,
stepDetail = $bindable(undefined),
workspace = $workspaceStore,
workspace = $operatingWorkspace,
minHeight = 400,
noBorder = false,
hideDefaultInputs = false,
@@ -12,7 +12,7 @@
import SchemaViewer from './SchemaViewer.svelte'
import { scriptPathToHref } from '$lib/scripts'
import { cleanExpr, copyToClipboard } from '$lib/utils'
import { hubBaseUrlStore, workspaceStore } from '$lib/stores'
import { hubBaseUrlStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import FlowModuleScript from './flows/content/FlowModuleScript.svelte'
@@ -20,6 +20,9 @@
import HighlightTheme from './HighlightTheme.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import FlowGraphViewerStepHeader from './FlowGraphViewerStepHeader.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
schema?: any | undefined
@@ -41,7 +44,7 @@
workspace = undefined,
onBack = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let codeViewer: Drawer | undefined = $state()
</script>
@@ -3,9 +3,11 @@
import { createEventDispatcher, untrack } from 'svelte'
import PopoverV2 from '$lib/components/meltComponents/Popover.svelte'
import HistoricInputs from './HistoricInputs.svelte'
import { workspaceStore } from '$lib/stores'
import { JobService } from '$lib/gen'
import { Button } from './common'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
path: string
@@ -27,7 +29,7 @@
async function loadInitial() {
loading = true
let jobs = await JobService.listJobs({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
scriptPathExact: path,
jobKinds: ['flow', 'flowpreview'].join(','),
perPage: 1
@@ -43,7 +45,7 @@
}
$effect(() => {
if ($workspaceStore && !newFlow) {
if ($operatingWorkspace && !newFlow) {
untrack(() => loadInitial())
}
})
@@ -11,7 +11,6 @@
Keyboard
} from 'lucide-svelte'
import { base } from '$lib/base'
import { workspaceStore } from '$lib/stores'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import LogViewer from './LogViewer.svelte'
import FlowLogViewer from './FlowLogViewer.svelte'
@@ -26,6 +25,9 @@
import { Tooltip } from './meltComponents'
import FlowTimelineBar from './FlowTimelineBar.svelte'
import { getActiveReplay } from './recording/replay.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
type RootJobData = Partial<Job>
@@ -96,7 +98,7 @@
function getJobLink(jobId: string | undefined): string {
if (!jobId) return ''
return `${base}/run/${jobId}?workspace=${workspaceId ?? $workspaceStore}`
return `${base}/run/${jobId}?workspace=${workspaceId ?? $operatingWorkspace}`
}
function getStatusColor(status: FlowStatusModule['type'] | undefined): string {
@@ -1,6 +1,5 @@
<script lang="ts">
import { type Job, JobService, type FlowModule, type RestartedFrom } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Button } from './common'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
@@ -11,6 +10,9 @@
import FlowProgressBar from './flows/FlowProgressBar.svelte'
import { CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
import type { Schema } from '$lib/common'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
open: boolean
@@ -165,7 +167,7 @@
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: opWorkspace?.() ?? $workspaceStore ?? '',
workspace: opWorkspace?.() ?? $operatingWorkspace ?? '',
id: jobId,
requestBody: {}
}))
@@ -6,7 +6,6 @@
type OpenFlow,
type ScriptLang
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Badge, Button } from './common'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import type { FlowEditorContext } from './flows/types'
@@ -44,6 +43,9 @@
import FlowRestartButton from './FlowRestartButton.svelte'
import { useNestedRestartState } from './useNestedRestartState.svelte'
import { buildFlowRecording, downloadRecordingJson } from './recording/runRecording'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
previewMode: 'upTo' | 'whole'
@@ -138,7 +140,7 @@
opWorkspace
} = $state(getContext<FlowEditorContext>('FlowEditorContext'))
// Acting workspace when previewing inside an AI session; else the nav workspace.
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
let opWs = $derived(opWorkspace?.() ?? $operatingWorkspace)
const dispatch = createEventDispatcher()
let renderCount: number = $state(0)
@@ -3,7 +3,9 @@
import Popover from './meltComponents/Popover.svelte'
import { Play, RefreshCw } from 'lucide-svelte'
import { FlowService, JobService, type FlowVersion } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
import { emptyString, sendUserToast } from '$lib/utils'
interface Props {
@@ -166,7 +168,7 @@
flow_version: flowVersion
}
let run = await JobService.restartFlowAtStep({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
id: jobId,
requestBody
})
@@ -178,7 +180,7 @@
loadingVersions = true
try {
flowVersions = await FlowService.getFlowHistory({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
path: flowPath
})
if (flowVersions.length > 0) {
@@ -4,8 +4,11 @@
import { setContext, untrack } from 'svelte'
import type { DurationStatus, FlowStatusViewerContext, GraphModuleState } from './graph'
import { isOwner as loadIsOwner, type StateStore } from '$lib/utils'
import { userStore, workspaceStore } from '$lib/stores'
import { userStore } from '$lib/stores'
import type { CompletedJob, FlowModule, FlowNote, FlowValue, Job } from '$lib/gen'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
jobId: string
@@ -98,7 +101,7 @@
})
function loadOwner(path: string) {
isOwner = loadIsOwner(path, $userStore!, workspaceId ?? $workspaceStore!)
isOwner = loadIsOwner(path, $userStore!, workspaceId ?? $operatingWorkspace!)
}
async function updateJobId() {
@@ -15,7 +15,6 @@
type FlowNote,
type FlowValue
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import FlowJobResult from './FlowJobResult.svelte'
import WorkflowTimeline from './WorkflowTimeline.svelte'
@@ -69,6 +68,9 @@
releaseLinkedToolsScope,
retainLinkedToolsScope
} from './flows/linkedAgentToolsStore.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
let {
flowState: flowStateStore,
@@ -168,7 +170,7 @@
isSubflow = false,
reducedPolling = false,
wideResults = false,
workspace = $workspaceStore,
workspace = $operatingWorkspace,
prefix = undefined,
topModuleStates = undefined,
refreshGlobal,
@@ -242,7 +244,7 @@
resourceMetadataCache[asset.path] = undefined
if (!isReplay) {
ResourceService.getResource({
workspace: workspace ?? $workspaceStore!,
workspace: workspace ?? $operatingWorkspace!,
path: asset.path
})
.then((r) => (resourceMetadataCache[asset.path] = r))
@@ -278,7 +280,7 @@
// resolving in the navigation one finds nothing, or an unrelated resource sharing the path. The
// store scope stays keyed on `workspace` to match what FlowGraphV2 reads — the job id in the key
// already makes the bucket unique.
let agentFetchWorkspace = $derived(workspaceId ?? job?.workspace_id ?? $workspaceStore)
let agentFetchWorkspace = $derived(workspaceId ?? job?.workspace_id ?? $operatingWorkspace)
// Hold this scope for as long as the viewer is mounted, so the store's cap can't drop tools the
// run still needs (nothing would refetch them — the set of linked steps hasn't changed).
$effect(() => {
@@ -627,7 +629,7 @@
) {
if (!isReplay) {
JobService.getJob({
workspace: workspaceId ?? $workspaceStore ?? '',
workspace: workspaceId ?? $operatingWorkspace ?? '',
id: mod.job ?? '',
noLogs: true,
noCode: true
@@ -722,7 +724,7 @@
})
if (!isReplay) {
JobService.getStartedAtByIds({
workspace: workspaceId ?? $workspaceStore ?? '',
workspace: workspaceId ?? $operatingWorkspace ?? '',
requestBody: missingStartedAtIds
})
.then((jobs) => {
@@ -1569,7 +1571,7 @@
let storedJob = storedListJobs[j]
if (!storedJob && !isReplay) {
storedJob = await JobService.getJob({
workspace: workspaceId ?? $workspaceStore ?? '',
workspace: workspaceId ?? $operatingWorkspace ?? '',
id: loopJobId,
noLogs: true,
noCode: true
@@ -2135,7 +2137,7 @@
id={isReplay ? undefined : job.id}
workspace={isReplay
? undefined
: (job.workspace_id ?? $workspaceStore ?? 'no_w')}
: (job.workspace_id ?? $operatingWorkspace ?? 'no_w')}
args={job.args}
/>
{:else}
@@ -2202,7 +2204,7 @@
id={isReplay ? undefined : node.job_id}
workspace={isReplay
? undefined
: (job.workspace_id ?? $workspaceStore ?? 'no_w')}
: (job.workspace_id ?? $operatingWorkspace ?? 'no_w')}
args={node.args}
/>
</div>
@@ -7,12 +7,14 @@
import { Loader2 } from 'lucide-svelte'
import { cleanValueProperties, replaceFalseWithUndefined } from '$lib/utils'
import { orderedYamlStringify } from '$lib/utils/orderedYaml'
import { workspaceStore } from '$lib/stores'
import { watch } from 'runed'
import HighlightTheme from './HighlightTheme.svelte'
import FlowViewerInner from './FlowViewerInner.svelte'
import FlowInputViewer from './FlowInputViewer.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface PreviousFlow {
summary: string
@@ -86,7 +88,7 @@
return
}
previousFlow = await FlowService.getFlowVersion({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
version
})
previousFlowCache[version] = previousFlow
+16 -18
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { userStore, workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import {
type Folder,
type FolderDefaultPermissionedAs,
@@ -89,8 +90,12 @@
workspace
}: Props = $props()
const targetWorkspace = $derived(workspace ?? $workspaceStore ?? '')
const aimedElsewhere = $derived(!!workspace && workspace !== $workspaceStore)
const operatingWorkspace = useOperatingWorkspace()
const targetWorkspace = $derived(workspace ?? $operatingWorkspace ?? '')
const aimedElsewhere = $derived(!!targetWorkspace && targetWorkspace !== $workspaceStore)
// The group editor and permission history act on the operating workspace and take no
// workspace of their own, so they can only follow a drawer aimed at that one.
const offOperating = $derived(!!targetWorkspace && targetWorkspace !== $operatingWorkspace)
// `$userStore` describes the workspace the app is *in*. Aimed at another one it answers
// the wrong question — a folder admin there would get read-only controls, and a
@@ -121,9 +126,9 @@
})
async function loadTargetUser(): Promise<void> {
if (!aimedElsewhere || !workspace) return
if (!aimedElsewhere) return
try {
targetUser = await UserService.whoami({ workspace })
targetUser = await UserService.whoami({ workspace: targetWorkspace })
} catch {
// Not a member, or the call failed: no membership means read-only controls,
// which is the safe reading — the write would be refused anyway.
@@ -551,7 +556,7 @@
let loadStarted = false
$effect.pre(() => {
if (loadStarted) return
if ($workspaceStore && $userStore) {
if (targetWorkspace && $userStore) {
loadStarted = true
untrack(() => {
load()
@@ -675,10 +680,7 @@
class="grow min-w-0"
>
{#snippet endSnippet({ item, close: closeSelect })}
<!-- GroupEditor reads and writes `$workspaceStore` and takes no workspace of its
own, so it cannot follow a drawer aimed at another one: viewing a group
there would edit the same-named group in the active workspace. -->
{#if ownerKind == 'group' && !aimedElsewhere}
{#if ownerKind == 'group' && !offOperating}
<Button
title="View group"
variant="subtle"
@@ -696,7 +698,7 @@
{/if}
{/snippet}
{#snippet bottomSnippet({ close: closeSelect })}
{#if ownerKind == 'group' && !aimedElsewhere}
{#if ownerKind == 'group' && !offOperating}
<Button
variant="subtle"
unifiedSize="sm"
@@ -838,11 +840,9 @@
</Cell>
<Cell last actions>
<div class="flex items-center justify-end">
<!-- The group editor reads `$workspaceStore`, so it can only be opened for the
workspace the app is in — see the picker's own buttons. It decides on its
own whether the group is editable here; a member with no write on it still
gets to see who is in it. -->
{#if ownerKindOf(perm.owner_name) === 'group' && !aimedElsewhere}
<!-- The group editor decides on its own whether the group is editable here; a
member with no write on it still gets to see who is in it. -->
{#if ownerKindOf(perm.owner_name) === 'group' && !offOperating}
<Button
title="Manage group"
variant="subtle"
@@ -1001,9 +1001,7 @@
</CollapseLink>
{/if}
<!-- PermissionHistory fetches against `$workspaceStore`; aimed elsewhere it would show
another folder's history entirely. -->
{#if !isNew && !aimedElsewhere && reloadHistory > 0}
{#if !isNew && !offOperating && reloadHistory > 0}
{#key reloadHistory}
<PermissionHistory
{name}
@@ -1,6 +1,7 @@
<script lang="ts">
import { FolderService, UserService, type User } from '$lib/gen'
import { workspaceStore, userStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import { isDemoWorkspaceRestricted } from '$lib/cloud'
import { ChevronDown, Pen, PlusIcon } from 'lucide-svelte'
import { Button } from './common'
@@ -39,14 +40,15 @@
workspace
}: Props = $props()
const targetWorkspace = $derived(workspace ?? $workspaceStore ?? '')
const operatingWorkspace = useOperatingWorkspace()
const targetWorkspace = $derived(workspace ?? $operatingWorkspace ?? '')
// `$userStore` describes the workspace the app is *in*. When this picker is aimed
// somewhere else, those memberships answer the wrong question — and since a folder
// without write access renders disabled, a stale answer makes the real folders
// unpickable. Resolve the membership for the workspace actually being listed.
let targetUser: User | undefined = $state(undefined)
const aimedElsewhere = $derived(!!workspace && workspace !== $workspaceStore)
const aimedElsewhere = $derived(!!targetWorkspace && targetWorkspace !== $workspaceStore)
const membership = $derived(aimedElsewhere ? targetUser : ($userStore ?? undefined))
const restricted = $derived(
@@ -129,9 +131,9 @@
}
async function loadTargetUser(): Promise<void> {
if (!workspace || workspace === $workspaceStore) return
if (!aimedElsewhere) return
try {
targetUser = await UserService.whoami({ workspace })
targetUser = await UserService.whoami({ workspace: targetWorkspace })
} catch {
// Not a member, or the call failed: every folder stays read-only, which is
// the safe reading — the import would be refused anyway.
@@ -1,5 +1,5 @@
<script lang="ts">
import { workspaceStore, enterpriseLicense, userStore } from '$lib/stores'
import { enterpriseLicense } from '$lib/stores'
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2, Github, RotateCw, Plus, Minus, Download, AlertTriangle } from 'lucide-svelte'
@@ -18,6 +18,14 @@
type GitHubAppState
} from '$lib/githubApp'
import RepositorySelector from './RepositorySelector.svelte'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
resourceType: string
@@ -70,14 +78,14 @@
let showGitHubApp = $derived(
resourceType === 'git_repository' &&
$workspaceStore &&
($userStore?.is_admin || $userStore?.is_super_admin)
$operatingWorkspace &&
(actingUser?.is_admin || actingUser?.is_super_admin)
)
// Load GitHub installations when conditions are met
$effect(() => {
if (showGitHubApp && $enterpriseLicense && $workspaceStore) {
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
if (showGitHubApp && $enterpriseLicense && $operatingWorkspace) {
loadGithubInstallations(githubState, $operatingWorkspace).catch((error) => {
console.error('Failed to load GitHub installations:', error)
})
}
@@ -113,11 +121,11 @@
}
async function handleDeleteInstallation(installationId: number) {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
try {
await deleteInstallation($workspaceStore, installationId, () =>
loadGithubInstallations(githubState, $workspaceStore!)
await deleteInstallation($operatingWorkspace, installationId, () =>
loadGithubInstallations(githubState, $operatingWorkspace!)
)
} catch (error) {
console.error('Failed to delete installation:', error)
@@ -125,11 +133,11 @@
}
async function handleAddInstallation(installationId: number, workspaceId: string) {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
try {
await addInstallationToWorkspace($workspaceStore, installationId, workspaceId, () =>
loadGithubInstallations(githubState, $workspaceStore!)
await addInstallationToWorkspace($operatingWorkspace, installationId, workspaceId, () =>
loadGithubInstallations(githubState, $operatingWorkspace!)
)
} catch (error) {
console.error('Failed to add installation:', error)
@@ -137,22 +145,22 @@
}
async function handleExportInstallation(installationId: number) {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
try {
await exportInstallation($workspaceStore, installationId)
await exportInstallation($operatingWorkspace, installationId)
} catch (error) {
console.error('Failed to export installation:', error)
}
}
async function handleImportInstallation() {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
try {
await importInstallation($workspaceStore, githubState.importJwt, () => {
await importInstallation($operatingWorkspace, githubState.importJwt, () => {
githubState.importJwt = ''
loadGithubInstallations(githubState, $workspaceStore!)
loadGithubInstallations(githubState, $operatingWorkspace!)
})
} catch (error) {
console.error('Failed to import installation:', error)
@@ -160,17 +168,17 @@
}
function handleRefreshInstallations() {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
loadGithubInstallations(githubState, $workspaceStore).catch((error) => {
loadGithubInstallations(githubState, $operatingWorkspace).catch((error) => {
console.error('Failed to refresh installations:', error)
})
}
function handleInstallClickWithPopover() {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
handleInstallClick(githubState, $workspaceStore, () => {
handleInstallClick(githubState, $operatingWorkspace, () => {
githubAppPopover?.open()
})
}
@@ -297,9 +305,9 @@
target="_blank"
disabled={githubState.isCheckingInstallation}
on:click={() => {
if ($workspaceStore) {
startInstallationCheck(githubState, $workspaceStore, () =>
loadGithubInstallations(githubState, $workspaceStore!)
if ($operatingWorkspace) {
startInstallationCheck(githubState, $operatingWorkspace, () =>
loadGithubInstallations(githubState, $operatingWorkspace!)
)
}
}}
@@ -1,5 +1,5 @@
<script lang="ts">
import { workspaceStore, userStore, enterpriseLicense } from '$lib/stores'
import { enterpriseLicense } from '$lib/stores'
import { GitSyncService, type GitlabProject } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import Popover from './meltComponents/Popover.svelte'
@@ -8,6 +8,14 @@
import TextInput from './text_input/TextInput.svelte'
import Select from './select/Select.svelte'
import { GitBranch, Gitlab, Loader2 } from 'lucide-svelte'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
resourceType: string
@@ -30,7 +38,7 @@
onArgsUpdate
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let baseUrl = $state('https://gitlab.com')
let token = $state('')
@@ -50,7 +58,7 @@
let show = $derived(
resourceType === 'git_repository' &&
!!ws &&
($userStore?.is_admin || $userStore?.is_super_admin)
(actingUser?.is_admin || actingUser?.is_super_admin)
)
// The project listing is served by an enterprise-only route, so on a build
// without it the form's first request would 404. The button still shows,
@@ -1,9 +1,11 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Popover } from './meltComponents'
import { GitBranch, Loader2 } from 'lucide-svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
isOpen?: boolean
@@ -20,12 +22,12 @@
let gitRepoResources = $state<{ path: string; description?: string }[]>([])
async function loadGitRepoResources() {
if (!$workspaceStore || loading) return
if (!$operatingWorkspace || loading) return
loading = true
try {
const resources = await ResourceService.listResource({
workspace: $workspaceStore,
workspace: $operatingWorkspace,
resourceType: 'git_repository'
})
@@ -39,7 +41,7 @@
}
$effect(() => {
if (isOpen && $workspaceStore) {
if (isOpen && $operatingWorkspace) {
untrack(() => loadGitRepoResources())
}
})
@@ -1,10 +1,12 @@
<script lang="ts">
import { HelpersService, ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Button, Drawer, DrawerContent } from './common'
import { GitBranch, Loader2, FolderOpen } from 'lucide-svelte'
import Select from './select/Select.svelte'
import { createEventDispatcher } from 'svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
open: boolean
@@ -27,7 +29,7 @@
workspace: workspaceProp = undefined
}: Props = $props()
let ws = $derived(workspaceProp ?? $workspaceStore)
let ws = $derived(workspaceProp ?? $operatingWorkspace)
const dispatch = createEventDispatcher<{
selected: {
@@ -3,11 +3,13 @@
import { untrack } from 'svelte'
import { Alert, Button } from './common'
import S3FilePickerInner from './S3FilePickerInner.svelte'
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import { Loader, Loader2, ChevronDown, ChevronRight, ExternalLink } from 'lucide-svelte'
import { hubPaths } from '$lib/hub'
import { sleep } from '$lib/utils'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const CLONE_MARKER_FILE = '.windmill_clone_complete'
const POLL_INTERVAL_MS = 1500
@@ -42,7 +44,7 @@
workspace: workspaceProp = undefined
}: Props = $props()
let ws = $derived(workspaceProp ?? $workspaceStore)
let ws = $derived(workspaceProp ?? $operatingWorkspace)
let commitHash = $derived(commitHashInput)
+30 -13
View File
@@ -6,7 +6,7 @@
type Group,
type InstanceGroup
} from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { userStore } from '$lib/stores'
import { onMount, tick, untrack } from 'svelte'
import { Button } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
@@ -34,6 +34,14 @@
type GroupDraft,
type GroupRole
} from '$lib/groupDraft'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
const ROLE_TOOLTIPS = {
member:
@@ -74,10 +82,18 @@
}: Props = $props()
const restricted = $derived(
isDemoWorkspaceRestricted($workspaceStore, $userStore?.is_admin, $userStore?.is_super_admin)
isDemoWorkspaceRestricted($operatingWorkspace, actingUser?.is_admin, actingUser?.is_super_admin)
)
let can_write = $state(false)
let groupPerms = $state<Record<string, boolean> | undefined>(undefined)
// A verdict that does not come from a loaded group: a new one is writable, one that failed
// to load is not.
let writeVerdict = $state<boolean | undefined>(undefined)
// Derived for the same reason as the trigger editors: the acting user's role can land after
// the group does, and a snapshot taken first would stay read-only.
const can_write = $derived(
writeVerdict ?? (groupPerms !== undefined && canWrite(name, groupPerms, actingUser))
)
let group: Group | undefined
let instance_group: InstanceGroup | undefined = $state()
let usernames: string[] = $state([])
@@ -123,11 +139,11 @@
}
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
usernames = await UserService.listUsernames({ workspace: $operatingWorkspace! })
}
async function loadGroupNames(): Promise<void> {
groupNames = (await GroupService.listGroupNames({ workspace: $workspaceStore! })) ?? []
groupNames = (await GroupService.listGroupNames({ workspace: $operatingWorkspace! })) ?? []
}
async function loadInstanceGroup(): Promise<void> {
@@ -142,7 +158,7 @@
loadAside(loadUsernames)
if (isNew) {
loadAside(loadGroupNames)
can_write = true
writeVerdict = true
setDraft(emptyDraft())
loaded = true
} else {
@@ -159,8 +175,9 @@
const apply = (value: GroupDraft) =>
opts?.baselineOnly ? (baseline = structuredClone(value)) : setDraft(value)
try {
group = await GroupService.getGroup({ workspace: $workspaceStore!, name })
can_write = canWrite(name, group.extra_perms ?? {}, $userStore)
group = await GroupService.getGroup({ workspace: $operatingWorkspace!, name })
groupPerms = group.extra_perms ?? {}
writeVerdict = undefined
apply({
summary: group.summary ?? '',
members: Array.from(
@@ -179,7 +196,7 @@
sendUserToast(e?.body ?? String(e), true)
// Only the opening read decides this. Revoking it on a failed reconcile would
// disable Save against a draft that is still dirty, with nothing left to reload.
if (!opts?.baselineOnly) can_write = false
if (!opts?.baselineOnly) writeVerdict = false
} finally {
loaded = true
}
@@ -245,7 +262,7 @@
* membership goes through the endpoints that name who was added or promoted — which is
* what the permission history reads back. The diff itself is in `groupDraft.ts`. */
async function applyMemberChanges(next: GroupDraft['members'], prev: GroupDraft['members']) {
const workspace = $workspaceStore ?? ''
const workspace = $operatingWorkspace ?? ''
for (const call of groupMemberDiff(prev, next, $userStore?.username)) {
switch (call.kind) {
case 'addUser':
@@ -289,7 +306,7 @@
try {
if (created) {
await GroupService.createGroup({
workspace: $workspaceStore ?? '',
workspace: $operatingWorkspace ?? '',
requestBody: { name, summary: next.summary }
})
alreadyCreated = true
@@ -299,7 +316,7 @@
} else {
if (next.summary !== prev.summary) {
await GroupService.updateGroup({
workspace: $workspaceStore ?? '',
workspace: $operatingWorkspace ?? '',
name,
requestBody: { summary: next.summary }
})
@@ -335,7 +352,7 @@
let loadStarted = false
$effect.pre(() => {
if (loadStarted) return
if ($workspaceStore && $userStore) {
if ($operatingWorkspace && $userStore) {
loadStarted = true
untrack(() => {
load()
@@ -7,7 +7,9 @@
import { DataTable } from '$lib/components/table'
import HistoricList from './HistoricList.svelte'
import { Loader2 } from 'lucide-svelte'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
runnableId?: string | undefined
@@ -32,7 +34,7 @@
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let historicList: HistoricList | undefined = $state(undefined)
const dispatch = createEventDispatcher()
@@ -1,9 +1,11 @@
<script lang="ts">
import { InputService, type RunnableType } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy, untrack } from 'svelte'
import InfiniteList from './InfiniteList.svelte'
import JobSchemaPicker from './schema/JobSchemaPicker.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
runnableId: string | undefined
@@ -52,7 +54,7 @@
refreshInterval()
loadInputsPageFn = async (page: number, perPage: number) => {
const inputs = await InputService.getInputHistory({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
runnableId,
runnableType,
page,
@@ -109,7 +111,7 @@
if (!id) return
const payloadData = await InputService.getArgsFromHistoryOrSavedInput({
jobOrInputId: id,
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
input,
allowLarge
})
@@ -122,7 +124,11 @@
}
$effect(() => {
$workspaceStore && runnableId && runnableType && infiniteList && untrack(() => initLoadInputs())
$operatingWorkspace &&
runnableId &&
runnableType &&
infiniteList &&
untrack(() => initLoadInputs())
})
</script>
@@ -1,11 +1,13 @@
<script lang="ts">
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { watch } from 'runed'
import { Plus } from 'lucide-svelte'
import { Button } from './common'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
/** The transforms being edited. A picked variable is written into `pickForField`'s. */
@@ -26,7 +28,7 @@
variableEditor = $bindable()
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
watch(
() => ws,
@@ -1,7 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { type InputTransform } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue, type DynamicInput as DynamicInputTypes } from '$lib/utils'
import { untrack } from 'svelte'
import StepInputsGen from './copilot/StepInputsGen.svelte'
@@ -12,6 +11,9 @@
import type ItemPicker from './ItemPicker.svelte'
import type VariableEditor from './VariableEditor.svelte'
import ResizeTransitionWrapper from './common/ResizeTransitionWrapper.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
schema: Schema | { properties?: Record<string, any> }
@@ -54,7 +56,7 @@
workspace
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let inputCheck: { [id: string]: boolean } = $state({})
+4 -2
View File
@@ -14,7 +14,6 @@
type WorkflowStatus,
type OpenFlow
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getViewToken } from '$lib/viewToken'
import { WM_LOGS_SKIPPED } from '$lib/consts'
import { getContext, onDestroy, tick, untrack } from 'svelte'
@@ -22,6 +21,9 @@
import { sendUserToast } from '$lib/toast'
import { DynamicInput, isScriptPreview } from '$lib/utils'
import { getActiveReplay, getReplayStartTime } from './recording/replay.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
// Will be set to number if job is not a flow
@@ -75,7 +77,7 @@
children
}: Props = $props()
let workspace = $derived(workspaceOverride ?? $workspaceStore)
let workspace = $derived(workspaceOverride ?? $operatingWorkspace)
let syncIteration: number = 0
let errorIteration = 0
@@ -1,9 +1,11 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { Alert, Skeleton } from './common'
import { Activity } from 'lucide-svelte'
import { JobService } from '$lib/gen'
import { msToReadableTime } from '$lib/utils'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
// OTEL SpanKind enum values from opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto
const SpanKind = {
@@ -54,14 +56,14 @@
})
async function loadTraces() {
if (!$workspaceStore || !jobId) return
if (!$operatingWorkspace || !jobId) return
loading = true
error = null
try {
const response = await JobService.getJobOtelTraces({
workspace: $workspaceStore,
workspace: $operatingWorkspace,
id: jobId
})
traces = response as unknown as OtelSpan[]
@@ -123,12 +125,18 @@
function getKindLabel(kind: number): string {
switch (kind) {
case SpanKind.INTERNAL: return 'Internal'
case SpanKind.SERVER: return 'Server'
case SpanKind.CLIENT: return 'Client'
case SpanKind.PRODUCER: return 'Producer'
case SpanKind.CONSUMER: return 'Consumer'
default: return 'Unknown'
case SpanKind.INTERNAL:
return 'Internal'
case SpanKind.SERVER:
return 'Server'
case SpanKind.CLIENT:
return 'Client'
case SpanKind.PRODUCER:
return 'Producer'
case SpanKind.CONSUMER:
return 'Consumer'
default:
return 'Unknown'
}
}
@@ -182,17 +190,15 @@
<Activity size={48} class="mb-4 opacity-50" />
<p class="text-lg font-medium">No HTTP requests captured</p>
<p class="text-sm mt-2">
This job did not make any HTTP/HTTPS requests, or HTTP Request Tracing is not enabled in instance settings.
This job did not make any HTTP/HTTPS requests, or HTTP Request Tracing is not enabled in
instance settings.
</p>
</div>
{:else}
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">Traces ({traces.length} spans)</h3>
<button
class="text-sm text-blue-600 hover:underline"
onclick={loadTraces}
>
<button class="text-sm text-blue-600 hover:underline" onclick={loadTraces}>
Refresh
</button>
</div>
@@ -219,10 +225,7 @@
{@const statusCode = span.status?.code ?? 0}
<div class="hover:bg-surface-hover">
<button
class="w-full px-4 py-2 text-left"
onclick={() => toggleSpan(span.span_id)}
>
<button class="w-full px-4 py-2 text-left" onclick={() => toggleSpan(span.span_id)}>
<div class="grid grid-cols-12 gap-2 items-center">
<div class="col-span-4 flex items-center gap-2">
<span class="text-xs text-secondary">
@@ -293,7 +296,9 @@
{#each Object.entries(parsedAttrs) as [key, value]}
<div class="flex gap-2">
<span class="text-secondary font-medium shrink-0">{key}:</span>
<span class="font-mono break-all">{typeof value === 'object' ? JSON.stringify(value) : value}</span>
<span class="font-mono break-all"
>{typeof value === 'object' ? JSON.stringify(value) : value}</span
>
</div>
{/each}
</div>
@@ -2,7 +2,9 @@
import Badge from './common/badge/Badge.svelte'
import Button from './common/button/Button.svelte'
import { Plus, Tag, X } from 'lucide-svelte'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
labels: string[] | undefined
@@ -53,7 +55,7 @@
async function loadExistingLabels() {
try {
const resp = await fetch(`/api/w/${workspace ?? $workspaceStore}/labels/list`)
const resp = await fetch(`/api/w/${workspace ?? $operatingWorkspace}/labels/list`)
if (resp.ok) existingLabels = await resp.json()
} catch {}
}
@@ -1,6 +1,5 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getContext, untrack } from 'svelte'
import DarkModeObserver from './DarkModeObserver.svelte'
import { Button, Drawer, DrawerContent } from './common'
@@ -11,6 +10,9 @@
import IconedResourceType from './IconedResourceType.svelte'
import { addResourceTitle } from './resourceTypeDisplay'
import { loadResourceTypeDisplayName } from './displayNameLoaders'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
value: string | undefined
@@ -35,7 +37,7 @@
let open = $state(false)
let refreshCount = $state(0)
const appViewerContext = getContext<AppViewerContext>('AppViewerContext')
let ws = $derived(workspace ?? appViewerContext?.workspace ?? $workspaceStore)
let ws = $derived(workspace ?? appViewerContext?.workspace ?? $operatingWorkspace)
let collection = $state(value ? [{ value, label: value }] : [])
@@ -139,7 +141,7 @@
title="App connection"
class="w-full h-full"
src="{base}/embed_connect?resource_type={resourceType}&workspace={appViewerContext?.workspace ??
$workspaceStore}&express=false"
$operatingWorkspace}&express=false"
/> -->
</DrawerContent>
</Drawer>
+6 -4
View File
@@ -18,7 +18,6 @@
import { withExternalDomain } from '$lib/externalDomain'
import { downloadViaClient, shouldDownloadViaClient } from '$lib/utils/downloadFile'
import { appendViewToken } from '$lib/viewToken'
import { workspaceStore } from '$lib/stores'
import { AnsiUp } from 'ansi_up'
import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
import { JobService } from '$lib/gen'
@@ -27,6 +26,9 @@
import Tooltip from './Tooltip.svelte'
import { twMerge } from 'tailwind-merge'
import QueuePosition from './QueuePosition.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
content: string | undefined
@@ -111,7 +113,7 @@
const id = jobId
fetchedSkippedJobId = id
untrack(() => {
JobService.getJob({ workspace: $workspaceStore ?? '', id })
JobService.getJob({ workspace: $operatingWorkspace ?? '', id })
.then((j) => {
if (fetchedSkippedJobId === id) {
const logs = (j as { logs?: string })['logs'] ?? ''
@@ -219,7 +221,7 @@
if (downloadStartUrl) {
scroll = false
let res = (await JobService.getLogFileFromStore({
workspace: $workspaceStore ?? '',
workspace: $operatingWorkspace ?? '',
path: downloadStartUrl
})) as string
LOG_LIMIT += Math.min(LOG_INC, res.length)
@@ -250,7 +252,7 @@
fetchedSkippedJobId = undefined
}
})
let logsApiPath = $derived(appendViewToken(`/w/${$workspaceStore}/jobs_u/get_logs/${jobId}`))
let logsApiPath = $derived(appendViewToken(`/w/${$operatingWorkspace}/jobs_u/get_logs/${jobId}`))
let downloadHref = $derived(withExternalDomain(`${base}/api${logsApiPath}`))
let downloadName = $derived(`windmill_logs_${jobId}.txt`)
let truncatedContent = $derived(
@@ -14,9 +14,11 @@
import type SimpleEditor from './SimpleEditor.svelte'
import { getResourceTypes } from './resourceTypesStore'
import { twMerge } from 'tailwind-merge'
import { workspaceStore } from '$lib/stores'
import { AGENT_FIELDS, initialVisibleAgentFields } from './flows/agentFormFields'
import { openAgentFields } from './flows/content/AiAgentStepInputs.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
schema: Schema | { properties?: Record<string, any>; required?: string[] }
@@ -47,7 +49,7 @@
const { stepsInputArgs, flowStateStore, flowStore, previewArgs, opWorkspace } =
getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
let opWs = $derived(opWorkspace?.() ?? $operatingWorkspace)
let inputCheck: { [id: string]: boolean } = $state({})
$effect(() => {
@@ -7,7 +7,6 @@
type JavascriptTransform,
type Job
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/scripts'
import { getContext, untrack } from 'svelte'
import type { FlowEditorContext } from './flows/types'
@@ -23,6 +22,9 @@
import { AGENT_FLOW_LOCAL_KEYS } from './flows/agentResourceUtils'
import { AGENT_HISTORY_KEYS } from './flows/agentFormFields'
import { sendUserToast } from '$lib/toast'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
mod: FlowModule
@@ -56,7 +58,7 @@
let previewBase = $derived($pathStore ?? '')
// Acting workspace when the flow editor runs in an AI session; else the nav workspace.
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
let opWs = $derived(opWorkspace?.() ?? $operatingWorkspace)
let jobLoader: JobLoader | undefined = $state(undefined)
let jobProgressReset: () => void = () => {}
@@ -10,9 +10,12 @@
import { AppService, HelpersService } from '$lib/gen'
import { base } from '$lib/base'
import { downloadViaClient, shouldDownloadViaClient } from '$lib/utils/downloadFile'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { enterpriseLicense } from '$lib/stores'
import { Download } from 'lucide-svelte'
import { Loader2 } from 'lucide-svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
@@ -53,7 +56,7 @@
// is set, else the viewer-scoped helpers. Same request/response shape either
// way — the only difference is which identity authorizes the S3 read.
function loadRowCount(searchCol: string | undefined, searchTerm: string | undefined) {
const workspace = workspaceId ?? $workspaceStore!
const workspace = workspaceId ?? $operatingWorkspace!
return appPath
? AppService.appLoadTableCount({
workspace,
@@ -82,7 +85,7 @@
searchTerm?: string
csvSeparator?: string
}) {
const workspace = workspaceId ?? $workspaceStore!
const workspace = workspaceId ?? $operatingWorkspace!
const csv = s3resource.endsWith('.csv')
if (appPath) {
const data = {
@@ -1,11 +1,14 @@
<script lang="ts">
import { VariableService } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { userStore } from '$lib/stores'
import { ephemeralSecretPrefix, mintEphemeralSecret } from './secretArgUtils'
import { sendUserToast } from '$lib/toast'
import { Button } from './common'
import Password from './Password.svelte'
import { untrack } from 'svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
value?: string | undefined
@@ -18,7 +21,7 @@
let { value = $bindable(undefined), disabled, minRows, workspace }: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let path = $state('')
// Workspace the variable at `path` actually lives in; `ws` can move away from it.
+37 -19
View File
@@ -26,7 +26,7 @@
AzureTriggerService,
EmailTriggerService
} from '$lib/gen'
import { superadmin, userStore, workspaceStore, type UserExt } from '$lib/stores'
import { superadmin, type UserExt } from '$lib/stores'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import { writable } from 'svelte/store'
import { Alert, Button } from './common'
@@ -45,6 +45,15 @@
import Select from './select/Select.svelte'
import { twMerge } from 'tailwind-merge'
import InputError from './InputError.svelte'
import {
useOperatingUser,
useOperatingWorkspace,
useOperatingWorkspaceHref
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const operatingHref = useOperatingWorkspaceHref()
type PathKind =
| 'resource'
@@ -81,15 +90,12 @@
disableEditing?: boolean
size?: 'sm' | 'md'
drawerOffset?: number
/** Workspace the folder list and path-existence checks run against.
* Defaults to the navigation `$workspaceStore`; pass the session's acting
* workspace when the editor operates on a workspace other than the one the
* top nav points at (see the sessions preview / dev-workspace flows). */
/** Workspace the folder list and path-existence checks run against. Defaults to the
* operating workspace (see `useOperatingWorkspace`). */
workspaceOverride?: string
/** The user acting in `workspaceOverride`, for the owner suggestion and the folder
* write flags. Omit it to stand in the navigation `$userStore`, who is a member of
* the navigation workspace only; pass `null` for "not known (yet)", which that user
* must not answer for either. */
* write flags. Omit it to stand in the user acting in the operating workspace; pass
* `null` for "not known (yet)", which no user must answer for. */
actingUser?: UserExt | null
/** One path that does not count as taken, for a caller creating something that may
* already have written there itself — a setup flow correcting its own failed attempt.
@@ -121,11 +127,11 @@
warnOnRename = true
}: Props = $props()
let ws = $derived(workspaceOverride ?? $workspaceStore)
// Sole place this component falls back to the ambient user, and only for a caller that
// passed none; everything below reads `user`, so a caller acting on another workspace is
// never mixed with the navigation user's memberships.
let user = $derived(actingUser === undefined ? $userStore : (actingUser ?? undefined))
let ws = $derived(workspaceOverride ?? $operatingWorkspace)
// Sole place this component falls back to an ambient user, and only for a caller that passed
// none: the one acting in `ws`, never the navigation user, whose memberships belong to
// another workspace. Everything below reads `user`.
let user = $derived(actingUser === undefined ? operatingUser.in(ws) : (actingUser ?? undefined))
$effect.pre(() => {
if (path == undefined) {
@@ -612,13 +618,13 @@
<Tooltip>
<ul>
{#each scripts || [] as path}
<li><a target="_blank" href="/scripts/edit/{path}">{path}</a></li>
<li><a target="_blank" href={operatingHref(`/scripts/edit/${path}`)}>{path}</a></li>
{/each}
{#each flows || [] as path}
<li><a target="_blank" href="/flows/edit/{path}">{path}</a></li>
<li><a target="_blank" href={operatingHref(`/flows/edit/${path}`)}>{path}</a></li>
{/each}
{#each apps || [] as path}
<li><a target="_blank" href="/apps/edit/{path}">{path}</a></li>
<li><a target="_blank" href={operatingHref(`/apps/edit/${path}`)}>{path}</a></li>
{/each}
</ul>
</Tooltip>
@@ -632,21 +638,33 @@
<ul class="list-disc">
{#each scripts || [] as scriptPath}
<li>
<a href={`/scripts/edit/${scriptPath}`} class="text-blue-400" target="_blank">
<a
href={operatingHref(`/scripts/edit/${scriptPath}`)}
class="text-blue-400"
target="_blank"
>
{scriptPath}
</a>
</li>
{/each}
{#each flows || [] as flowPath}
<li>
<a href={`/flows/edit/${flowPath}`} class="text-blue-400" target="_blank">
<a
href={operatingHref(`/flows/edit/${flowPath}`)}
class="text-blue-400"
target="_blank"
>
{flowPath}
</a>
</li>
{/each}
{#each apps || [] as appPath}
<li>
<a href={`/apps/edit/${appPath}`} class="text-blue-400" target="_blank">
<a
href={operatingHref(`/apps/edit/${appPath}`)}
class="text-blue-400"
target="_blank"
>
{appPath}
</a>
</li>
@@ -87,7 +87,9 @@
<script lang="ts">
import TextInput from '$lib/components/text_input/TextInput.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
import { untrack } from 'svelte'
type Props = {
@@ -101,9 +103,8 @@
error?: string | boolean
textInputClass?: string
onkeyup?: (e: KeyboardEvent) => void
/** Workspace whose paths feed the autocomplete. Defaults to the navigation
* `$workspaceStore`; pass the acting workspace when the editor operates on
* a workspace other than the one the top nav points at. */
/** Workspace whose paths feed the autocomplete. Defaults to the operating workspace
* (see `useOperatingWorkspace`). */
workspace?: string
}
@@ -121,7 +122,7 @@
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let inputEl: TextInput | undefined = $state(undefined)
export function focus() {
@@ -1,9 +1,11 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { untrack } from 'svelte'
import TableCustom from './TableCustom.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Label from './Label.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface PermissionChange {
id?: number
@@ -31,10 +33,10 @@
let perPage = $state(50)
async function loadHistory() {
if (!$workspaceStore) return
if (!$operatingWorkspace) return
loading = true
try {
history = await fetchHistory($workspaceStore, name, page, perPage)
history = await fetchHistory($operatingWorkspace, name, page, perPage)
} catch (e) {
console.error('Failed to load permission history:', e)
history = []
@@ -56,7 +58,7 @@
}
$effect.pre(() => {
if ($workspaceStore && name) {
if ($operatingWorkspace && name) {
untrack(() => {
loadHistory()
})
@@ -80,13 +82,13 @@
{:else}
<TableCustom>
{#snippet headerRow()}
<tr >
<tr>
<th>Changed By</th>
<th>Change Type</th>
<th>Affected</th>
<th>Date</th>
</tr>
{/snippet}
{/snippet}
{#snippet body()}
<tbody>
{#each history as change}
@@ -1,8 +1,10 @@
<script lang="ts">
import { JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { forLater, getDbClockNow } from '$lib/forLater'
import { displayDate } from '$lib/utils'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
let {
jobId,
@@ -15,7 +17,7 @@
let fetchingQueuePosition = false
let workspace = $derived(workspaceId ?? $workspaceStore)
let workspace = $derived(workspaceId ?? $operatingWorkspace)
let scheduledFor = $state(undefined) as undefined | number
@@ -9,7 +9,6 @@
} from '$lib/gen'
import { canWrite } from '$lib/utils'
import { createEventDispatcher, onDestroy, untrack } from 'svelte'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { clearJsonSchemaResourceCache } from './schema/jsonSchemaResource.svelte'
import ResourceForm from './ResourceForm.svelte'
@@ -22,6 +21,9 @@
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import { setLocalDraftHint } from '$lib/localDraftHints.svelte'
import { onUserInput } from '$lib/userDraftEditGate'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
canSave?: boolean
@@ -74,7 +76,7 @@
// Sole ambient read in this file: the acting workspace is an input, and only its
// default comes from the navigation store.
let effectiveWorkspace = $derived(workspace ?? $workspaceStore!)
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace!)
// Fallback to `effectiveWorkspace` insulates against reactify-style
// parents that re-spread props without `selected` — otherwise it
// transiently resets and the form below remounts on every keystroke.
@@ -471,6 +473,12 @@
current.path = npath
}
/** The path the resource has in `ws`: after a save, the one it was saved under. Each
* workspace-specific version keeps its own, so the selected one says nothing about `ws`. */
export function pathIn(ws: string): string | undefined {
return initialStates[ws]?.path
}
/** Whether the write landed. It toasts its own failure, so most callers ignore this;
* one that follows the save with bookkeeping of its own has to know not to. */
export async function save(): Promise<boolean> {
@@ -5,13 +5,13 @@
import { History, Loader2, Save } from 'lucide-svelte'
import WsSpecificVersions from './WsSpecificVersions.svelte'
import { workspaceStore } from '$lib/stores'
import { isOwner } from '$lib/utils'
import { useActingUser } from '$lib/actingUser.svelte'
import LocalDraftBanner from './LocalDraftBanner.svelte'
import OpenInSessionButton from './sessions/OpenInSessionButton.svelte'
import {
clearPageDrawerAnchor,
handOffPageDrawer,
pageDrawerSessionSource,
setPageDrawerAnchor
} from './sessions/pageDrawerSession'
@@ -20,10 +20,15 @@
import IconedResourceType from './IconedResourceType.svelte'
import { addResourceTitle } from './resourceTypeDisplay'
import { loadResourceTypeDisplayName } from './displayNameLoaders'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
let {
workspace = undefined,
disableChatOffset = false,
inline = false,
onClose = undefined,
onRestored = undefined,
onSaved = undefined
}: {
@@ -31,10 +36,17 @@
* once, at `effectiveWorkspace`, and nowhere else in this file. */
workspace?: string
disableChatOffset?: boolean
/** Render in place, filling the parent, with no drawer or close button — for a host
* that gives the editor a whole pane. Saving and restoring then leave it open: the
* host remounts it on what was written. */
inline?: boolean
/** With `inline`, closes whatever hosts the editor; the header has a close button only when set. */
onClose?: () => void
onRestored?: () => void
/** Fires after Save has written, for a caller showing state derived from the
* resource — `onRestored` only covers restoring an old version. */
onSaved?: () => void
* resource — `onRestored` only covers restoring an old version. `path` is where the
* resource now lives in this drawer's workspace, undefined when the save failed. */
onSaved?: (path: string | undefined) => void
} = $props()
let drawer: Drawer | undefined = $state()
@@ -45,7 +57,8 @@
let resourceEditor:
| {
save: () => void
save: () => Promise<boolean>
pathIn: (ws: string) => string | undefined
localDraftDeployed: () => unknown
localDraftCurrent: () => unknown
discardLocalDraft: () => void
@@ -58,7 +71,7 @@
let selected: string | undefined = $state(undefined)
let viewJsonSchema = $state(false)
let effectiveWorkspace = $derived(workspace ?? $workspaceStore!)
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace!)
// The editor renders whichever workspace-specific variant `selected` points at, so history has
// to follow it too — otherwise a restore would write over the variant the user is not looking at.
let historyWorkspace = $derived(selected ?? effectiveWorkspace)
@@ -85,6 +98,7 @@
* dedicated editor elsewhere: the generic form would render its configuration field by field,
* and materialize a default into every one the value leaves out. */
export async function initEdit(p: string, opts?: { json?: boolean }): Promise<void> {
if (handOffPageDrawer(RESOURCES_PATH, p)) return
// A `close({ keepAnchor })` on an already-closed drawer emits no close event, so the flag
// would still be standing when the next drawer session ends and would swallow that one's
// anchor clear. Every session starts having to clear its own.
@@ -126,22 +140,35 @@
)
</script>
<Drawer
bind:this={drawer}
size="50rem"
{disableChatOffset}
on:close={() => {
if (keepAnchorOnClose) {
keepAnchorOnClose = false
return
}
clearPageDrawerAnchor(RESOURCES_PATH)
}}
>
{#if inline}
<!-- ResourceEditor reads its path once, at mount — a drawer mounts it only when opened. -->
{#if path !== undefined || resource_type !== undefined}
{@render content()}
{/if}
{:else}
<Drawer
bind:this={drawer}
size="50rem"
{disableChatOffset}
on:close={() => {
if (keepAnchorOnClose) {
keepAnchorOnClose = false
return
}
clearPageDrawerAnchor(RESOURCES_PATH)
}}
>
{@render content()}
</Drawer>
{/if}
{#snippet content()}
<DrawerContent
title={mode == 'edit' ? 'Edit ' + path : addResourceTitle(resource_type)}
bannerReserved={mode == 'edit'}
on:close={drawer?.closeDrawer}
hideClose={inline && !onClose}
fullScreen={!inline}
on:close={() => (inline ? onClose?.() : drawer?.closeDrawer())}
>
{#snippet titleExtra()}
{#if mode == 'new' && resource_type}
@@ -201,10 +228,10 @@
// Closed before the write is awaited, the way it always was: `save()` toasts its
// own failures and never rejects, so waiting would only add visible lag to every
// caller of this drawer. `onSaved` still fires after the write lands.
const saved = resourceEditor?.save()
const editor = resourceEditor
const saved = editor?.save()
drawer?.closeDrawer()
await saved
onSaved?.()
onSaved?.((await saved) ? editor?.pathIn(effectiveWorkspace) : undefined)
}}
disabled={!canSave}
>
@@ -212,7 +239,7 @@
</Button>
{/snippet}
</DrawerContent>
</Drawer>
{/snippet}
<Drawer bind:this={historyDrawer} size="1200px">
<DrawerContent title="Versions History" on:close={historyDrawer?.closeDrawer} noPadding>
@@ -8,7 +8,7 @@
import { Alert, Skeleton } from './common'
import Path from './Path.svelte'
import LabelsInput from './LabelsInput.svelte'
import { workspaceStore, type UserExt } from '$lib/stores'
import { type UserExt } from '$lib/stores'
import SchemaForm from './SchemaForm.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import FilesetEditor from './FilesetEditor.svelte'
@@ -22,6 +22,9 @@
import SyncResourceTypes from './SyncResourceTypes.svelte'
import Label from './Label.svelte'
import ResourcePathHint from './ResourcePathHint.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
path: string
@@ -81,7 +84,7 @@
onCredentialStored
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let rawCode: string | undefined = $state(undefined)
let textFileContent: string = $state('')
@@ -1,6 +1,5 @@
<script lang="ts">
import { ResourceService, WorkspaceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onMount, untrack } from 'svelte'
import AppConnect from './AppConnectDrawer.svelte'
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
@@ -12,6 +11,9 @@
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import DropdownV2 from './DropdownV2.svelte'
import { appIconComponent } from './icons'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
initialValue?: string | undefined
@@ -62,7 +64,7 @@
onValueChange = undefined
}: Props = $props()
let effectiveWorkspace = $derived(workspace ?? $workspaceStore!)
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace!)
if (initialValue && value == undefined) {
value = initialValue
@@ -2,7 +2,6 @@
import { createEventDispatcher } from 'svelte'
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import IconedResourceType from './IconedResourceType.svelte'
import { Button, ClearableInput } from './common'
import Label from './Label.svelte'
@@ -14,6 +13,9 @@
setResourceTypeDisplayNames,
sortResourceTypesByMatch
} from './resourceTypeDisplay'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
value: string | undefined
notPickable?: boolean
@@ -25,7 +27,7 @@
let resources: { name: string; description?: string; searchText: string }[] = $state([])
async function loadResources() {
const types = await ResourceService.listResourceType({ workspace: $workspaceStore! })
const types = await ResourceService.listResourceType({ workspace: $operatingWorkspace! })
setResourceTypeDisplayNames(types)
resources = types.map((t) => ({
name: t.name,
@@ -42,7 +44,7 @@
}
$effect(() => {
if ($workspaceStore) {
if ($operatingWorkspace) {
untrack(() => {
loadResources()
})
@@ -1,7 +1,6 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { ResourceService, type ResourceVersion } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Skeleton } from '$lib/components/common'
import Button from './common/button/Button.svelte'
import HighlightCode from './HighlightCode.svelte'
@@ -12,6 +11,9 @@
import VersionListItem from './VersionListItem.svelte'
import { displayDate } from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
let {
path,
@@ -27,7 +29,7 @@
onRestore?: () => void
} = $props()
let effectiveWorkspace = $derived(workspace ?? $workspaceStore!)
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace!)
let versions = $state<ResourceVersion[] | undefined>(undefined)
// Which row is highlighted, updated on click. `loaded` is dropped the moment the selection
@@ -7,7 +7,9 @@
import Select from './select/Select.svelte'
import { FileUp } from 'lucide-svelte'
import { SettingService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
import { resource } from 'runed'
interface Props {
@@ -54,7 +56,7 @@
* asset lives in a different workspace than this picker was mounted for. */
let workspaceOverride: string | undefined = $state(undefined)
let effectiveWorkspace = $derived(workspaceOverride ?? workspace)
let ws = $derived(effectiveWorkspace ?? $workspaceStore)
let ws = $derived(effectiveWorkspace ?? $operatingWorkspace)
let uploadModalOpen = $state(false)
let allFilesByKey: Record<
@@ -10,7 +10,6 @@
Trash,
MoveRight
} from 'lucide-svelte'
import { workspaceStore } from '$lib/stores'
import {
CancelablePromise,
HelpersService,
@@ -48,6 +47,9 @@
import FileUploadModal from './common/fileUpload/FileUploadModal.svelte'
import S3FilePreview from './S3FilePreview.svelte'
import { twMerge } from 'tailwind-merge'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
let deletionModalOpen = $state(false)
let fileDeletionInProgress = $state(false)
@@ -137,7 +139,7 @@
testConnectionRequest = HelpersService.datasetStorageTestConnection
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let rootPath = $state(initialRootPath)
let rootPathNestingLevel = $derived(1 * (rootPath.split('/').length - 1))
@@ -8,7 +8,6 @@
// need to hand it a `fileKey`. CSV separator/header are local state so
// the user can re-preview the same file with different parsing flags.
import { FileX2, Loader2 } from 'lucide-svelte'
import { workspaceStore } from '$lib/stores'
import {
HelpersService,
type CancelablePromise,
@@ -20,6 +19,9 @@
import { displayDate, displaySize, emptyString } from '$lib/utils'
import { twMerge } from 'tailwind-merge'
import ExpandableImage from '$lib/components/common/image/ExpandableImage.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
fileKey: string | undefined
@@ -115,7 +117,7 @@
// existence after an upstream run completes — moving from the
// "not yet materialized" empty state to the actual preview without
// requiring the user to re-click the asset.
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
$effect(() => {
const key = fileKey
@@ -1,12 +1,14 @@
<script lang="ts">
import { displayDate } from '$lib/utils.js'
import { InputService, type CreateInput, type RunnableType } from '$lib/gen/index.js'
import { workspaceStore } from '$lib/stores.js'
import { Button } from '$lib/components/common'
import { Save } from 'lucide-svelte'
import { sendUserToast } from '$lib/utils.js'
import { createEventDispatcher } from 'svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const dispatch = createEventDispatcher()
@@ -30,7 +32,7 @@
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let savingInputs = $state(false)
@@ -1,7 +1,7 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { InputService, type Input, type RunnableType } from '$lib/gen/index.js'
import { userStore, workspaceStore } from '$lib/stores.js'
import { userStore } from '$lib/stores.js'
import { sendUserToast } from '$lib/utils.js'
import { createEventDispatcher, onDestroy, untrack } from 'svelte'
import { Trash2, Save, Pencil } from 'lucide-svelte'
@@ -12,6 +12,14 @@
import InfiniteList from './InfiniteList.svelte'
import { twMerge } from 'tailwind-merge'
import SavedInputsPickerViewer from './SavedInputsPickerViewer.svelte'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
previewArgs?: any
@@ -36,7 +44,7 @@
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
interface EditableInput extends Input {
isEditing?: boolean
@@ -254,8 +262,8 @@
{#snippet children({ item, hover })}
{@const editOptions =
item.created_by == $userStore?.username ||
$userStore?.is_admin ||
$userStore?.is_super_admin}
actingUser?.is_admin ||
actingUser?.is_super_admin}
<Cell>
<div class="center-center">
<Save size={12} />
@@ -4,7 +4,6 @@
const bubble = createBubbler()
import type { Schema } from '$lib/common'
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue, computeShow, type DynamicInput } from '$lib/utils'
import { Button } from './common'
import ItemPicker from './ItemPicker.svelte'
@@ -26,6 +25,9 @@
import type { ComponentCustomCSS } from './apps/types'
import ResizeTransitionWrapper from './common/ResizeTransitionWrapper.svelte'
import { twMerge } from 'tailwind-merge'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
schema: Schema | any
@@ -122,7 +124,7 @@
actions: actions_render = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
const dispatch = createEventDispatcher()
@@ -30,6 +30,10 @@
workerTags,
workspaceStore
} from '$lib/stores'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
import {
emptySchema,
emptyString,
@@ -189,7 +193,10 @@
// (forked) workspace, so an embedded editor acts on the session's fork rather
// than the navigation workspace ($workspaceStore, which stays put). indicatorPath
// is the matching draft path (URL path full-page, session target in preview).
const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore)
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
const opWorkspace = $derived(autosaveWorkspace ?? $operatingWorkspace)
const indicatorPath = $derived(autosavePath ?? userDraftPath)
// The shared `workerTags` store caches tags for the navigation workspace. A
@@ -223,8 +230,8 @@
let preserveOnBehalfOf = $state(false)
const WM_DEPLOYERS_GROUP = 'wm_deployers'
let isDeployer = $derived($userStore?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
let canPreserve = $derived(!!$userStore?.is_admin || !!$userStore?.is_super_admin || isDeployer)
let isDeployer = $derived(actingUser?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
let canPreserve = $derived(!!actingUser?.is_admin || !!actingUser?.is_super_admin || isDeployer)
let originalOnBehalfOfEmail = $derived(savedScript?.on_behalf_of_email)
let originalOnBehalfOfPermissionedAs = $derived(savedScript?.on_behalf_of)
let onBehalfOfChoice: OnBehalfOfChoice = $state(undefined)
@@ -740,7 +747,7 @@
await deployTriggers(
triggersToDeploy,
opWorkspace,
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
!!actingUser?.is_admin || !!actingUser?.is_super_admin,
usedTriggerKinds,
script.path,
true
@@ -1158,7 +1165,7 @@
on:confirmed={handleDraftTriggersConfirmed}
/>
{#if !$userStore?.operator}
{#if !actingUser?.operator}
<Drawer
placement="right"
bind:open={metadataOpen}
@@ -11,7 +11,7 @@
type ScriptLang,
type ScriptModule
} from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { enterpriseLicense, userStore } from '$lib/stores'
import { copyToClipboard, emptySchema, sendUserToast } from '$lib/utils'
import Editor from './Editor.svelte'
import { inferArgs, inferAssets, inferAnsibleExecutionMode } from '$lib/infer'
@@ -129,6 +129,9 @@
import { resource, watch } from 'runed'
import { buildScriptRecording, downloadRecordingJson } from './recording/runRecording'
import DropdownV2 from './DropdownV2.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
// Exported
@@ -226,10 +229,8 @@
// built by the pipeline page from the resolved graph. Absent outside the
// pipeline editor — the check still runs, just without suppression.
schemaContractContext?: SchemaContractGraphContext
// Workspace to scope this editor's calls to. Defaults to the nav
// `$workspaceStore`; an AI-session live editor passes the session's
// acting workspace (a fork) so tests, captures and toolbar lookups hit
// the right workspace instead of the nav one.
// Workspace to scope this editor's calls to (tests, captures, toolbar lookups).
// Defaults to the operating workspace (see `useOperatingWorkspace`).
workspaceOverride?: string
}
@@ -278,7 +279,7 @@
workspaceOverride = undefined
}: Props = $props()
let opWs = $derived(workspaceOverride ?? $workspaceStore)
let opWs = $derived(workspaceOverride ?? $operatingWorkspace)
// Publish this editor's hand-off for AI entry points below it (the preview
// panel's "AI Fix"), withheld under `disableAi` so an embed that turned AI off
@@ -1,7 +1,6 @@
<script lang="ts">
import { ScriptService, FlowService, type Script, AppService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import { createEventDispatcher, untrack } from 'svelte'
@@ -18,6 +17,9 @@
import FlowIcon from './home/FlowIcon.svelte'
import DarkModeObserver from './DarkModeObserver.svelte'
import { truncate } from '$lib/utils'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
initialPath?: string | undefined
@@ -30,9 +32,8 @@
allowEdit?: boolean
allowView?: boolean
clearable?: boolean
/** Workspace to list runnables from. Defaults to the navigation
* `$workspaceStore`; pass the session's acting workspace so a forked
* session lists its own scripts/flows/apps rather than the parent's. */
/** Workspace to list runnables from. Defaults to the operating workspace (see
* `useOperatingWorkspace`). */
workspace?: string
}
@@ -50,10 +51,11 @@
workspace = undefined
}: Props = $props()
let effectiveWorkspace = $derived(workspace ?? $workspaceStore)
// Only carry the workspace onto Edit/View routes when an explicit override
// was passed, so existing callers' links are unchanged.
let wsParam = $derived(workspace ? `?workspace=${encodeURIComponent(workspace)}` : '')
let effectiveWorkspace = $derived(workspace ?? $operatingWorkspace)
// Edit/View routes open in the workspace listed here, not wherever the tab lands.
let wsParam = $derived(
effectiveWorkspace ? `?workspace=${encodeURIComponent(effectiveWorkspace)}` : ''
)
let items: { value: string; label: string }[] = $state([])
let drawerViewer: Drawer | undefined = $state()
@@ -2,7 +2,6 @@
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { emptyString } from '$lib/utils'
import { ScriptService, type ScriptHistory } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Skeleton } from '$lib/components/common'
import FlowModuleScript from './flows/content/FlowModuleScript.svelte'
import { createEventDispatcher } from 'svelte'
@@ -11,6 +10,9 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import VersionListItem from './VersionListItem.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const dispatch = createEventDispatcher()
@@ -27,7 +29,7 @@
async function loadVersions() {
loading = true
versions = await ScriptService.getScriptHistoryByPath({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
path: scriptPath
})
loading = false
@@ -42,7 +44,7 @@
return
}
await ScriptService.updateScriptHistory({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
path: scriptPath,
hash: scriptHash,
requestBody: {
+4 -2
View File
@@ -3,7 +3,6 @@
import Button from './common/button/Button.svelte'
import { runScriptAndPollResult } from './jobs/utils'
import { writingJobOptions } from './jobs/writingJob'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { untrack } from 'svelte'
import { getLanguageByResourceType } from './apps/components/display/dbtable/utils'
@@ -14,6 +13,9 @@
import { wrapDucklakeQuery } from './ducklake'
import { splitSqlStatements, pruneComments } from './sqlDdl'
import DdlMigrationGuard from './DdlMigrationGuard.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
type Props = {
input: DbInput
@@ -39,7 +41,7 @@
workspace = undefined,
tag = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
let dbType = $derived(getDbType(input))
// A datatable REPL targets `datatable://<name>`; surface DDL statements as
@@ -4,13 +4,21 @@
import Popover from '$lib/components/meltComponents/Popover.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
import Path from '$lib/components/Path.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { userStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { updateItemPathAndSummary, checkFlowOnBehalfOf } from './moveRenameManager'
import Label from './Label.svelte'
import LabelsInput from './LabelsInput.svelte'
import InheritedLabels from './InheritedLabels.svelte'
import Badge from './common/badge/Badge.svelte'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
summary?: string
@@ -36,7 +44,11 @@
let editPath = $state('')
let dirtyPath = $state(false)
let popoverOpen = $state(false)
let own = $state(false)
let ownPath = $state<string | undefined>(undefined)
// Derived: ownership answers about the operating workspace, whose user resolves asynchronously.
const own = $derived(
ownPath === undefined ? false : isOwner(ownPath, actingUser, $operatingWorkspace)
)
let onBehalfOfEmail = $state<string | undefined>(undefined)
let summaryInput: ReturnType<typeof TextInput> | undefined = $state()
let labelsDirty = $state(false)
@@ -47,10 +59,10 @@
editSummary = summary ?? ''
editPath = path ?? ''
labelsDirty = false
own = isOwner(path ?? '', $userStore, $workspaceStore)
ownPath = path ?? ''
onBehalfOfEmail = undefined
if (kind === 'flow' && $workspaceStore && path) {
checkFlowOnBehalfOf($workspaceStore, path).then((email) => {
if (kind === 'flow' && $operatingWorkspace && path) {
checkFlowOnBehalfOf($operatingWorkspace, path).then((email) => {
onBehalfOfEmail = email
})
}
@@ -63,7 +75,7 @@
try {
await updateItemPathAndSummary({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
kind,
initialPath,
newPath,
@@ -13,7 +13,9 @@
import Button from './common/button/Button.svelte'
import Tooltip from './meltComponents/Tooltip.svelte'
import { sendUserToast } from '$lib/toast'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
import { tryEvery } from '$lib/utils'
interface Props {
@@ -268,7 +270,7 @@ export async function main(bucket: any, api_token: string) {
loading = true
const resourceScript = scripts[resourceType]
const workspace = workspaceOverride ?? $workspaceStore!
const workspace = workspaceOverride ?? $operatingWorkspace!
const objectStorageArgs: Record<string, any> | undefined =
resourceType in objectStorageBody ? objectStorageBody[resourceType](args) : undefined
@@ -1,13 +1,13 @@
<script lang="ts">
import { VariableService, WorkspaceService } from '$lib/gen'
import { createEventDispatcher, untrack } from 'svelte'
import { workspaceStore } from '$lib/stores'
import { Button } from './common'
import Drawer from './common/drawer/Drawer.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import OpenInSessionButton from './sessions/OpenInSessionButton.svelte'
import {
clearPageDrawerAnchor,
handOffPageDrawer,
pageDrawerSessionSource,
setPageDrawerAnchor
} from './sessions/pageDrawerSession'
@@ -25,6 +25,9 @@
import LocalDraftBanner from './LocalDraftBanner.svelte'
import { isEncryptedDraftValue } from '$lib/encryptedDraft'
import { setLocalDraftHint } from '$lib/localDraftHints.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const dispatch = createEventDispatcher()
@@ -38,10 +41,25 @@
// The "current" workspace this editor defaults New/Edit actions to. Session
// editors pass their acting workspace so secrets are created/updated there
// rather than in the navigation workspace.
let { workspace = undefined }: { workspace?: string } = $props()
let {
workspace = undefined,
inline = false,
onClose = undefined,
onSaved = undefined
}: {
workspace?: string
/** Render in place, filling the parent, with no drawer or close button — for a host
* that gives the editor a whole pane. */
inline?: boolean
/** With `inline`, closes whatever hosts the editor; the header has a close button only when set. */
onClose?: () => void
/** Fires once a save lands, with the path the variable now lives at in `workspace` —
* not in the workspace-specific version selected, which can be another's. */
onSaved?: (path: string) => void
} = $props()
// Sole ambient read in this file: the acting workspace is an input, and only its
// default comes from the navigation store.
let curWs = $derived(workspace ?? $workspaceStore)
let curWs = $derived(workspace ?? $operatingWorkspace)
let editPath: string | undefined = $state(undefined)
@@ -233,6 +251,7 @@
}
export function editVariable(edit_path: string): void {
if (handOffPageDrawer(VARIABLES_PATH, edit_path)) return
reset()
editPath = edit_path
selected = curWs!
@@ -256,6 +275,7 @@
async function save(): Promise<void> {
const dirty = dirtyWorkspaces
const savedPath = (curWs ? states[curWs]?.draft?.path : undefined) ?? editPath ?? ''
try {
for (const ws of dirty) {
const s = states[ws].draft!
@@ -303,6 +323,7 @@
}
sendUserToast(edit ? `Updated variable in ${dirty.length} workspace(s)` : `Created variable`)
dispatch('create')
onSaved?.(savedPath)
drawer?.closeDrawer()
} catch (err) {
sendUserToast(`Could not save variable: ${err.body}`, true)
@@ -310,11 +331,21 @@
}
</script>
<Drawer bind:this={drawer} size="50rem" on:close={() => clearPageDrawerAnchor(VARIABLES_PATH)}>
{#if inline}
{@render content()}
{:else}
<Drawer bind:this={drawer} size="50rem" on:close={() => clearPageDrawerAnchor(VARIABLES_PATH)}>
{@render content()}
</Drawer>
{/if}
{#snippet content()}
<DrawerContent
title={edit ? `Update variable at ${initialPath}` : 'Add a variable'}
bannerReserved={edit}
on:close={drawer?.closeDrawer}
hideClose={inline && !onClose}
fullScreen={!inline}
on:close={() => (inline ? onClose?.() : drawer?.closeDrawer())}
>
{#snippet banner()}
<LocalDraftBanner
@@ -382,4 +413,4 @@
</Button>
{/snippet}
</DrawerContent>
</Drawer>
{/snippet}
@@ -10,10 +10,13 @@
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { Loader2, RotateCcw } from 'lucide-svelte'
import autosize from '$lib/autosize'
import { workspaceStore, type UserExt } from '$lib/stores'
import { type UserExt } from '$lib/stores'
import { isOwner } from '$lib/utils'
import { isEncryptedDraftValue } from '$lib/encryptedDraft'
import EncryptedDraftField from './EncryptedDraftField.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Variable {
value: string
@@ -56,7 +59,7 @@
actingUser
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let ws = $derived(workspace ?? $operatingWorkspace)
// Loading the deployed secret overwrites the draft row this form shares with the AI
// chat, so every path that would trigger it has to be blocked while that row stages a
@@ -2,6 +2,7 @@
import { Button } from '$lib/components/common'
import { ExternalLink, RotateCw, Loader2 } from 'lucide-svelte'
import { workerTags, workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import AssignableTags from './AssignableTags.svelte'
import { WorkerService } from '$lib/gen'
import WorkerTagSelect from './WorkerTagSelect.svelte'
@@ -11,8 +12,8 @@
popupPlacement?: 'bottom-end' | 'top-end'
disabled?: boolean
placeholder?: string
// Workspace to read tags from; defaults to $workspaceStore. A fork-scoped
// session passes its effective workspace so the picker matches the deploy target.
// Workspace to read tags from; defaults to the operating workspace (see
// `useOperatingWorkspace`).
workspaceId?: string
}
@@ -26,8 +27,11 @@
// See WorkerTagSelect: the shared `workerTags` cache is navigation-scoped, so a
// different target workspace reads/writes a local list to avoid clobbering it.
let effectiveWorkspace = $derived(workspaceId ?? $workspaceStore)
let usesLocal = $derived(workspaceId != undefined && workspaceId !== $workspaceStore)
const operatingWorkspace = useOperatingWorkspace()
let effectiveWorkspace = $derived(workspaceId ?? $operatingWorkspace)
let usesLocal = $derived(
effectiveWorkspace != undefined && effectiveWorkspace !== $workspaceStore
)
let localWorkerTags = $state<string[] | undefined>(undefined)
let currentTags = $derived(usesLocal ? localWorkerTags : $workerTags)
@@ -1,5 +1,6 @@
<script lang="ts">
import { workerTags, workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import { WorkerService } from '$lib/gen'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
@@ -31,10 +32,8 @@
/** Forwarded to the underlying Select — controls the input height. The
* condensed session top bar passes `sm` to match its smaller buttons. */
size?: 'sm' | 'md' | 'lg'
// Workspace to read custom tags and worker availability from. Defaults to
// $workspaceStore. Session editors act on a workspace that differs from the
// navigation one, so they pass their effective workspace to keep the tag
// list and availability dots matching the deploy target.
// Workspace to read custom tags and worker availability from. Defaults to the
// operating workspace (see `useOperatingWorkspace`).
workspaceId?: string
} = $props()
@@ -46,8 +45,11 @@
// The shared `workerTags` store caches tags for the navigation workspace. When
// this select targets a different workspace, read/write a local list instead so
// it neither shows the navigation workspace's tags nor clobbers the shared cache.
let effectiveWorkspace = $derived(workspaceId ?? $workspaceStore)
let usesLocal = $derived(workspaceId != undefined && workspaceId !== $workspaceStore)
const operatingWorkspace = useOperatingWorkspace()
let effectiveWorkspace = $derived(workspaceId ?? $operatingWorkspace)
let usesLocal = $derived(
effectiveWorkspace != undefined && effectiveWorkspace !== $workspaceStore
)
let localWorkerTags = $state<string[] | undefined>(undefined)
let currentTags = $derived(usesLocal ? localWorkerTags : $workerTags)
@@ -9,11 +9,19 @@
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import { CheckCircle2, XCircle } from 'lucide-svelte'
import { JobService, type Job, type WorkflowStatus } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { enterpriseLicense } from '$lib/stores'
import { Button } from '$lib/components/common'
import { Alert } from '$lib/components/common'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { sendUserToast } from '$lib/toast'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
flow_status: Record<string, WorkflowStatus>
@@ -97,7 +105,7 @@
}
async function fetchChildJob(id: string) {
const ws = $workspaceStore
const ws = $operatingWorkspace
if (!ws) return
loadingJobs[id] = true
try {
@@ -126,7 +134,7 @@
let approvalFormArgs: Record<string, Record<string, any>> = $state({})
async function handleApprove(key: string, formSchema: any) {
const ws = $workspaceStore
const ws = $operatingWorkspace
if (!ws || !jobId) return
approvalLoading[key] = true
try {
@@ -147,7 +155,7 @@
let cancelLoading = $state(false)
async function handleCancel() {
const ws = $workspaceStore
const ws = $operatingWorkspace
if (!ws || !jobId) return
cancelLoading = true
try {
@@ -260,7 +268,7 @@
<span class="text-tertiary">{msToSec(v.duration_ms ?? 0)}s</span>
{/if}
</div>
{#if canApprove && selfApprovalDisabled && $userStore?.is_admin}
{#if canApprove && selfApprovalDisabled && actingUser?.is_admin}
<div class="mt-1 ml-5 text-yellow-600 text-2xs">
Self-approval is disabled but allowed because you are an admin/owner
</div>
@@ -14,7 +14,6 @@ standalone editor autosaves and surfacing those in the breadcrumb picker
would be surprising.
-->
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import { untrack } from 'svelte'
import {
@@ -32,6 +31,9 @@ would be surprising.
} from '$lib/components/copilot/chat/global/userDraftAdapter'
import { isGlobalAiEnabled } from '$lib/components/copilot/chat/global/gate'
import { resource } from 'runed'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
type Kind = WorkspaceItemKind
type ScopeKind = Kind | 'all'
@@ -52,9 +54,8 @@ would be surprising.
externalFilter?: string
autoFocus?: boolean
flush?: boolean
// Load items and drafts from this workspace instead of the navigation
// workspace. Set by session live editors, whose acting workspace can
// differ from $workspaceStore; falls back to $workspaceStore otherwise.
// Load items and drafts from this workspace; defaults to the operating workspace
// (see `useOperatingWorkspace`).
workspaceId?: string
}
@@ -70,7 +71,7 @@ would be surprising.
workspaceId
}: Props = $props()
const effectiveWorkspace = $derived(workspaceId ?? $workspaceStore)
const effectiveWorkspace = $derived(workspaceId ?? $operatingWorkspace)
let inner = $state<DrillPickerHandle | undefined>(undefined)
@@ -22,7 +22,6 @@
type Scorer,
type ScorerMean
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { onDestroy, onMount, untrack } from 'svelte'
import {
@@ -52,6 +51,9 @@
subjectLabel,
type EvalsLocation
} from './evalUtils'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
/** A dataset is capped at this many cases, so one page holds the whole set. */
const CASE_PAGE_SIZE = 1000
@@ -84,7 +86,7 @@
active?: boolean
} = $props()
let ws = $derived(opWorkspace ?? $workspaceStore)
let ws = $derived(opWorkspace ?? $operatingWorkspace)
let datasets = $state<EvalDataset[]>([])
let dataset = $state<EvalDataset | undefined>(undefined)
let selectedDataset = $state<string | undefined>(undefined)
@@ -2,7 +2,7 @@
import { Alert } from '$lib/components/common'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { enterpriseLicense, userStore } from '$lib/stores'
import { Loader2 } from 'lucide-svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
@@ -26,6 +26,14 @@
import { canUserBypassRuleKind, protectionRulesState } from '$lib/workspaceProtectionRules.svelte'
import { FRONTEND_SDK_SCOPES } from '$lib/components/raw_apps/sdkScopes'
import { logFeatureUsage } from '$lib/utils/featureUsage'
import {
useOperatingUser,
useOperatingWorkspace
} from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspaceStore = useOperatingWorkspace()
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
const WM_DEPLOYERS_GROUP = 'wm_deployers'
@@ -74,29 +82,27 @@
* (`/secret_of/...` 404s with no `app` row) and renders a placeholder
* instead of the eternally-spinning link. */
newApp?: boolean
/** Workspace the app is deployed to — the session's acting workspace when
* embedded in a session preview, else the navigation `$workspaceStore`.
* The secret-URL / custom-path / folder / on-behalf-of lookups must target
* it, not `$workspaceStore` (which stays on the nav workspace in a session). */
/** Workspace the app is deployed to. The secret-URL / custom-path / folder /
* on-behalf-of lookups target it; defaults to the operating workspace. */
operatingWorkspace?: string
} = $props()
const opWs = $derived(operatingWorkspace ?? $workspaceStore)
const opWs = $derived(operatingWorkspace ?? $operatingWorkspaceStore)
let isDeployer = $derived($userStore?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
let isDeployer = $derived(actingUser?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
// Admins always pass the backend check. For everyone else, fail closed
// while the workspace protection rules are still loading so the toggle
// is never briefly enabled for a user the rules will end up restricting.
let rulesetsLoaded = $derived(protectionRulesState.rulesets !== undefined)
let canSetAnonymous = $derived(
!!$userStore?.is_admin ||
!!$userStore?.is_super_admin ||
!!actingUser?.is_admin ||
!!actingUser?.is_super_admin ||
(rulesetsLoaded &&
canUserBypassRuleKind('RestrictAnonymousAppDeployment', $userStore ?? undefined))
)
let canSetGuest = $derived(
!!$userStore?.is_admin ||
!!$userStore?.is_super_admin ||
!!actingUser?.is_admin ||
!!actingUser?.is_super_admin ||
(rulesetsLoaded &&
canUserBypassRuleKind('RestrictGuestAppDeployment', $userStore ?? undefined))
)
@@ -143,7 +149,7 @@
setPublishState()
}
}
let canPreserve = $derived(!!$userStore?.is_admin || !!$userStore?.is_super_admin || isDeployer)
let canPreserve = $derived(!!actingUser?.is_admin || !!actingUser?.is_super_admin || isDeployer)
let savedOnBehalfOfEmail = $derived(savedApp?.policy?.on_behalf_of_email)
let savedOnBehalfOf = $derived(savedApp?.policy?.on_behalf_of)
let onBehalfOfChoice: OnBehalfOfChoice = $state(undefined)
@@ -596,7 +602,7 @@
{/if}
<div class="mt-4">
{#if !($userStore?.is_admin || $userStore?.is_super_admin)}
{#if !(actingUser?.is_admin || actingUser?.is_super_admin)}
<Alert type="warning" title="Admin only" size="xs">
Custom path can only be set by workspace admins
</Alert>
@@ -617,7 +623,7 @@
options={{
right: 'Use a custom URL'
}}
disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)}
disabled={!$enterpriseLicense || !(actingUser?.is_admin || actingUser?.is_super_admin)}
/>
{#if customPath !== undefined}
@@ -625,7 +631,7 @@
<div>Custom path</div>
</div>
<input
disabled={!($userStore?.is_admin || $userStore?.is_super_admin)}
disabled={!(actingUser?.is_admin || actingUser?.is_super_admin)}
type="text"
autocomplete="off"
bind:value={customPath}
@@ -7,7 +7,6 @@
import JobArgs from '$lib/components/JobArgs.svelte'
import LogViewer from '$lib/components/LogViewer.svelte'
import { workspaceStore } from '$lib/stores'
import { BellOff, Loader2, RefreshCw } from 'lucide-svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { classNames, truncateRev, isFlowPreview } from '../../../utils'
@@ -21,6 +20,9 @@
import type { Job } from '$lib/gen'
import type { JobById } from '../types'
import { createEventDispatcher, untrack } from 'svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
open?: boolean
@@ -163,7 +165,7 @@
<Pane size={90} minSize={10} class="text-sm text-secondary">
<div class="relative h-full px-2">
<DisplayResult
workspaceId={$workspaceStore}
workspaceId={$operatingWorkspace}
jobId={selectedJobId}
result={jobResult.result}
/>
@@ -192,7 +194,7 @@
<div class="p-2">
<JobArgs
id={job?.id}
workspace={job?.workspace_id ?? $workspaceStore ?? 'no_w'}
workspace={job?.workspace_id ?? $operatingWorkspace ?? 'no_w'}
args={job?.args}
/>
</div>
@@ -219,7 +221,7 @@
{#if job != undefined && 'result' in job && job?.result != undefined}<div
class="relative h-full px-2"
><DisplayResult
workspaceId={$workspaceStore}
workspaceId={$operatingWorkspace}
jobId={selectedJobId}
result={job?.result}
/></div
@@ -240,7 +242,7 @@
{#if job != undefined && 'result' in job && job?.result != undefined}
<div class="relative h-full px-2">
<DisplayResult
workspaceId={$workspaceStore}
workspaceId={$operatingWorkspace}
jobId={selectedJobId}
result={jobResult?.transformer}
/>
@@ -14,12 +14,14 @@
} from '$lib/utils'
import { orderedYamlStringify } from '$lib/utils/orderedYaml'
import { AppService, type AppWithLastVersion, type AppHistory } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Skeleton } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { Pencil, ArrowRight, X, Loader2 } from 'lucide-svelte'
import Select from '$lib/components/select/Select.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
appPath: string | undefined
@@ -48,7 +50,7 @@
return cached
}
const app = await AppService.getAppByVersion({ workspace: $workspaceStore!, id: version })
const app = await AppService.getAppByVersion({ workspace: $operatingWorkspace!, id: version })
versionCache[version] = app
return app
}
@@ -60,7 +62,7 @@
loading = true
versions = await AppService.getAppHistoryByPath({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
path: appPath
})
loading = false
@@ -90,7 +92,7 @@
return
}
await AppService.updateAppHistory({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
id: appId,
version: appVersion,
requestBody: {
@@ -107,7 +109,9 @@
}
function toVersionLabel(version: AppHistory): string {
return emptyString(version.deployment_msg) ? `Version ${version.version}` : version.deployment_msg!
return emptyString(version.deployment_msg)
? `Version ${version.version}`
: version.deployment_msg!
}
let availableVersions = $derived(
@@ -19,7 +19,6 @@
CtxAppInput
} from '../../inputType'
import type { AppViewerContext } from '../../types'
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import { deepEqual } from 'fast-equals'
import { computeFields } from './utils'
@@ -35,6 +34,9 @@
import FlowEditorDrawer from '$lib/components/flows/content/FlowEditorDrawer.svelte'
import { FlowService, ScriptService, type OpenFlow } from '$lib/gen'
import { replaceScriptPlaceholderWithItsValues } from '$lib/hub'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
runnable: RunnableByPath
@@ -131,7 +133,7 @@
return
}
const loaded = await loadSchema($workspaceStore ?? '', runnable.path, 'flow')
const loaded = await loadSchema($operatingWorkspace ?? '', runnable.path, 'flow')
const schema = loaded?.schema ?? emptySchema()
if (!deepEqual(runnable.schema, schema)) {
runnable.schema = schema
@@ -167,7 +169,7 @@
async function openScriptEditor(path: string) {
try {
const script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
path
})
scriptEditorDrawer?.openDrawer(script.hash, () => {
@@ -344,7 +346,7 @@
startIcon={{ icon: Eye }}
endIcon={{ icon: ExternalLink }}
target="_blank"
href="{base}/flows/get/{runnable.path}?workspace={$workspaceStore}"
href="{base}/flows/get/{runnable.path}?workspace={$operatingWorkspace}"
>
Details
</Button>
@@ -425,7 +427,7 @@
{#if runnable.runType == 'flow' && isHubFlowPath(runnable.path)}
Hub flow not found at {runnable.path}
{:else}
{runnable.runType} not found at {runnable.path} in workspace {$workspaceStore}
{runnable.runType} not found at {runnable.path} in workspace {$operatingWorkspace}
{/if}
</div>
{:else if runnable.runType == 'script' || runnable.runType == 'hubscript'}
@@ -11,15 +11,12 @@
import type { Schema } from '$lib/common'
import { emptySchema } from '$lib/utils'
import { loadSchema } from '$lib/infer'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
import { buildPathRunnableSelection } from './runnableSelectorUtils'
type TabType =
| 'hubscripts'
| 'hubflows'
| 'workspacescripts'
| 'workspaceflows'
| 'inlinescripts'
type TabType = 'hubscripts' | 'hubflows' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
interface Props {
defaultUserInput?: boolean
@@ -60,7 +57,7 @@
path: string,
runType: 'script' | 'flow' | 'hubscript'
): Promise<{ schema: Schema; summary: string | undefined }> {
const schema = await loadSchema($workspaceStore!, path, runType)
const schema = await loadSchema($operatingWorkspace!, path, runType)
if (!schema.schema.order) {
schema.schema.order = Object.keys(schema.schema.properties ?? {})
}
@@ -7,9 +7,11 @@
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import { FlowService, type Flow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { emptyString } from '$lib/utils'
import { Skeleton } from '$lib/components/common'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
filter?: string
@@ -26,7 +28,7 @@
async function loadFlow(): Promise<void> {
const loadedFlows = await FlowService.listFlows({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
perPage: 300,
withoutDescription: true
})
@@ -7,9 +7,11 @@
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import { type Script, ScriptService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { emptyString } from '$lib/utils'
import { Skeleton } from '$lib/components/common'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
filter?: string
@@ -26,7 +28,7 @@
async function loadScripts(): Promise<void> {
const loadedScripts = await ScriptService.listScripts({
workspace: $workspaceStore!,
workspace: $operatingWorkspace!,
perPage: 300,
withoutDescription: true
})
@@ -48,7 +48,10 @@
import AssetRunsPanel from './AssetRunsPanel.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { fade } from 'svelte/transition'
import { userStore } from '$lib/stores'
import { useOperatingUser } from '$lib/components/operatingWorkspace.svelte'
const operatingUser = useOperatingUser()
const actingUser = $derived(operatingUser.current)
interface Props {
// Regular selection — loads the script by path for inline editing.
@@ -662,7 +665,7 @@
// hand-rolled here because we need *two* confirm buttons, not one.
let removeOpen = $state(false)
let removing = $state(false)
let canHardDelete = $derived(!!($userStore?.is_admin || $userStore?.is_super_admin))
let canHardDelete = $derived(!!(actingUser?.is_admin || actingUser?.is_super_admin))
// React to the parent's remove-signal counter and pop the same modal
// the in-pane trash button uses. Skipped for drafts (the parent calls
@@ -19,12 +19,15 @@
XCircle
} from 'lucide-svelte'
import type { ScriptLang } from '$lib/gen'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { enterpriseLicense } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import { PIPELINE_LANGUAGES } from './pipelineLanguages'
import type { PipelineOutputKind } from './pipelineTemplates'
import type { DbtAssetProvenance } from './types'
import DbtIcon from '$lib/components/icons/DbtIcon.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
// Shape used for both the data prop and the run callback. Drafts carry
// `content` / `language` so the page-level run handler can dispatch to
@@ -118,7 +121,7 @@
async function runProducers(e: MouseEvent) {
e.stopPropagation()
if (!$workspaceStore || running || !data.onRunProducer) return
if (!$operatingWorkspace || running || !data.onRunProducer) return
if (scriptProducers.length === 0) return
running = true
const handler = data.onRunProducer
@@ -170,14 +173,14 @@
}
const cols = Object.entries(d.columns ?? {})
if (cols.length) {
lines.push(
`columns: ${cols.map(([c, desc]) => (desc ? `${c} (${desc})` : c)).join(', ')}`
)
lines.push(`columns: ${cols.map(([c, desc]) => (desc ? `${c} (${desc})` : c)).join(', ')}`)
}
if (d.freshness) {
const f = d.freshness as Record<string, { count?: number; period?: string }>
const window = (k: string) =>
f[k]?.count != null ? `${k.replace('_after', '')} after ${f[k].count}${f[k].period?.[0] ?? ''}` : ''
f[k]?.count != null
? `${k.replace('_after', '')} after ${f[k].count}${f[k].period?.[0] ?? ''}`
: ''
const windows = ['warn_after', 'error_after'].map(window).filter(Boolean)
if (windows.length) lines.push(`freshness: ${windows.join(', ')}`)
}
@@ -8,7 +8,6 @@
// seconds so the user sees status transitions without refreshing —
// JobLoader handles streaming for the *selected* job.
import { JobService, type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy, untrack } from 'svelte'
import { displayDate } from '$lib/utils'
import { CheckCircle2, Clock, History, Loader2, XCircle, Ban } from 'lucide-svelte'
@@ -21,6 +20,9 @@
import { Popover } from '$lib/components/meltComponents'
import { twMerge } from 'tailwind-merge'
import DispatchEventsButton from '$lib/components/runs/DispatchEventsButton.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
// Producers of this asset. Each contributes its own listExtendedJobs
@@ -88,7 +90,7 @@
// Run only when the *content* of the producer set changes (string
// key) or when the parent explicitly bumps `refreshKey` after
// dispatching a new run. The producerKey debounce alone isn't
// enough: refresh() synchronously reads $workspaceStore and
// enough: refresh() synchronously reads $operatingWorkspace and
// runnableProducers before the first await, and Svelte 5 records
// those as deps of the surrounding $effect — which would then
// re-fire whenever the parent re-derives `producers` (i.e. on
@@ -174,11 +176,11 @@
async function refresh(): Promise<void> {
if (refreshInFlight) return refreshInFlight
if (!$workspaceStore || runnableProducers.length === 0) {
if (!$operatingWorkspace || runnableProducers.length === 0) {
jobs = []
return
}
const ws = $workspaceStore
const ws = $operatingWorkspace
// Capture the producer paths *now* — using runnableProducers
// directly inside the await would re-read after the array
// identity churned, defeating the in-flight guard.
@@ -286,12 +288,12 @@
</span>
<a
class="text-3xs text-blue-600 hover:underline shrink-0"
href={`${base}/run/${selectedJob.id}?workspace=${$workspaceStore}`}
href={`${base}/run/${selectedJob.id}?workspace=${$operatingWorkspace}`}
target="_blank">Open ↗</a
>
{#if $workspaceStore}
{#if $operatingWorkspace}
<DispatchEventsButton
workspace={selectedJob.workspace_id ?? $workspaceStore}
workspace={selectedJob.workspace_id ?? $operatingWorkspace}
jobId={selectedJob.id}
/>
{/if}
@@ -397,7 +399,7 @@
<div class="flex flex-col gap-3 p-3">
<JobArgs
id={selectedJob.id}
workspace={selectedJob.workspace_id ?? $workspaceStore ?? ''}
workspace={selectedJob.workspace_id ?? $operatingWorkspace ?? ''}
args={selectedJob.args}
/>
<div class="flex flex-col gap-1">
@@ -9,7 +9,6 @@
// just be noise.
import DBTable from '$lib/components/DBTable.svelte'
import { resource } from 'runed'
import { workspaceStore } from '$lib/stores'
import { loadAllTablesMetaData } from '$lib/components/apps/components/display/dbtable/metadata'
import { dbTableOpsWithPreviewScripts } from '$lib/components/dbOps'
import { WorkspaceService } from '$lib/gen'
@@ -19,6 +18,9 @@
import Button from '$lib/components/common/button/Button.svelte'
import { base } from '$lib/base'
import { twMerge } from 'tailwind-merge'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
// Asset path as parsed from `datatable://<datatable>/<table>` —
@@ -71,7 +73,7 @@
// query, and the user needs to configure it before pipeline scripts
// can write to it.
let datatables = resource(
() => $workspaceStore,
() => $operatingWorkspace,
async (ws) => {
if (!ws) return [] as string[]
try {
@@ -117,9 +119,9 @@
() => [input, refreshKey],
async ([_input]) => {
colDefsError = undefined
if (!_input || !$workspaceStore) return undefined
if (!_input || !$operatingWorkspace) return undefined
try {
return await loadAllTablesMetaData($workspaceStore, _input)
return await loadAllTablesMetaData($operatingWorkspace, _input)
} catch (e) {
colDefsError = (e as Error)?.message || String(e)
return undefined
@@ -138,12 +140,12 @@
})
let dbTableOps = $derived(
input && tableColDefs && parsed.table && $workspaceStore
input && tableColDefs && parsed.table && $operatingWorkspace
? dbTableOpsWithPreviewScripts({
input,
tableKey: parsed.table,
colDefs: tableColDefs,
workspace: $workspaceStore
workspace: $operatingWorkspace
})
: undefined
)
@@ -9,7 +9,6 @@
// ("This partition") or show the full table ("Whole table") via a toggle.
import DBTable from '$lib/components/DBTable.svelte'
import { resource } from 'runed'
import { workspaceStore } from '$lib/stores'
import { loadAllTablesMetaData } from '$lib/components/apps/components/display/dbtable/metadata'
import { dbTableOpsWithPreviewScripts } from '$lib/components/dbOps'
import type { DbInput } from '$lib/components/dbTypes'
@@ -18,6 +17,9 @@
import { twMerge } from 'tailwind-merge'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
// Full asset URI, e.g. `ducklake://main/orders_daily`.
@@ -61,9 +63,9 @@
() => [input, refreshKey] as const,
async ([_input]) => {
colDefsError = undefined
if (!_input || !$workspaceStore) return undefined
if (!_input || !$operatingWorkspace) return undefined
try {
return await loadAllTablesMetaData($workspaceStore, _input)
return await loadAllTablesMetaData($operatingWorkspace, _input)
} catch (e) {
colDefsError = (e as Error)?.message || String(e)
return undefined
@@ -89,12 +91,12 @@
})
let dbTableOps = $derived.by(() => {
if (!(input && tableColDefs && tableKey && $workspaceStore)) return undefined
if (!(input && tableColDefs && tableKey && $operatingWorkspace)) return undefined
const ops = dbTableOpsWithPreviewScripts({
input,
tableKey,
colDefs: tableColDefs,
workspace: $workspaceStore,
workspace: $operatingWorkspace,
whereClause
})
// Read-only preview: drop the mutation handlers so DBTable hides its
@@ -7,7 +7,6 @@
// hand is shown above the grid so the affordance is self-documenting.
import DBTable from '$lib/components/DBTable.svelte'
import { resource } from 'runed'
import { workspaceStore } from '$lib/stores'
import {
fetchDucklakeColumnsAtVersion,
dbTableOpsWithPreviewScripts
@@ -17,6 +16,9 @@
import { AlertTriangle, Loader2, ClipboardCopy } from 'lucide-svelte'
import { Button } from '$lib/components/common'
import { twMerge } from 'tailwind-merge'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
// Full asset URI, e.g. `ducklake://main/orders_daily`.
@@ -56,9 +58,10 @@
let columns = resource(
() => [ducklake, tableKey, version] as const,
async ([_ducklake, _tableKey, _version]) => {
if (!_ducklake || !_tableKey || _version == undefined || !$workspaceStore) return undefined
if (!_ducklake || !_tableKey || _version == undefined || !$operatingWorkspace)
return undefined
const colDefs = await fetchDucklakeColumnsAtVersion({
workspace: $workspaceStore,
workspace: $operatingWorkspace,
ducklake: _ducklake,
tableKey: _tableKey,
version: _version
@@ -73,13 +76,13 @@
let tableColDefs = $derived(ready ? columns.current!.colDefs : undefined)
let dbTableOps = $derived.by(() => {
if (!(input && tableColDefs && tableKey && $workspaceStore && version != undefined))
if (!(input && tableColDefs && tableKey && $operatingWorkspace && version != undefined))
return undefined
const ops = dbTableOpsWithPreviewScripts({
input,
tableKey,
colDefs: tableColDefs,
workspace: $workspaceStore,
workspace: $operatingWorkspace,
version
})
// Historical reads are immutable: drop every mutation handler so DBTable
@@ -6,7 +6,9 @@
import PipelineRunForm from './PipelineRunForm.svelte'
import AssetRunsPanel from './AssetRunsPanel.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
import { parsePipelineAnnotations } from './parsePipelineAnnotations'
interface Props {
@@ -201,7 +203,7 @@
bind:args
bind:isValid
{partitionSpec}
workspace={$workspaceStore ?? ''}
workspace={$operatingWorkspace ?? ''}
{materializeTarget}
{upstreamAssets}
/>
@@ -1,5 +1,4 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import type { NativeTriggerKind } from './types'
@@ -24,7 +23,10 @@
import EmailTriggerEditor from '$lib/components/triggers/email/EmailTriggerEditor.svelte'
import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte'
import WebhookEditor from '$lib/components/triggers/webhook/WebhookEditor.svelte'
import { setTriggerWorkspace } from '$lib/components/triggers/triggerWorkspace'
import { setOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
// Owns the native-trigger drawer wiring for the pipeline canvas: the nine
// editor instances, the create/edit dispatch by kind, and the delete
@@ -40,9 +42,8 @@
type Props = { onUpdate: () => void; mountTriggerEditors: boolean; workspace?: string }
let { onUpdate, mountTriggerEditors, workspace: triggerWorkspace }: Props = $props()
// Register the trigger-workspace resolver for the whole editor subtree (the
// nine editors + the delete handler below). See triggerWorkspace.ts.
setTriggerWorkspace(() => triggerWorkspace ?? $workspaceStore)
// The nine editors below act on this workspace (see operatingWorkspace.svelte.ts).
setOperatingWorkspace(() => triggerWorkspace)
let kafkaEditor: KafkaTriggerEditor | undefined = $state()
let mqttEditor: MqttTriggerEditor | undefined = $state()
@@ -127,7 +128,7 @@
}
async function confirmDeleteAttachedTrigger() {
const workspace = triggerWorkspace ?? $workspaceStore
const workspace = triggerWorkspace ?? $operatingWorkspace
if (!triggerDeleteTarget || !workspace) return
const { kind, path: triggerPath } = triggerDeleteTarget
triggerDeleteLoading = true
@@ -28,7 +28,9 @@
import DropdownV2 from '$lib/components/DropdownV2.svelte'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import type { Item } from '$lib/utils'
import { workspaceStore } from '$lib/stores'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
import { sendUserToast, msToReadableTimeShort } from '$lib/utils'
interface Props {
@@ -129,7 +131,7 @@
async function runSelf(e: MouseEvent, cascade?: boolean) {
e.stopPropagation()
if (!$workspaceStore || running || !data.onRunSelf) return
if (!$operatingWorkspace || running || !data.onRunSelf) return
running = true
try {
await data.onRunSelf(cascade != undefined ? { cascade } : undefined)

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