windmill-components 1.698.0 → 1.699.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package/components/DisplayResult.svelte +39 -19
  2. package/package/components/FlowStatusViewerInner.svelte +23 -9
  3. package/package/components/HistoricInputs.svelte +2 -1
  4. package/package/components/InstanceSetting.svelte +47 -5
  5. package/package/components/LogViewer.svelte +62 -23
  6. package/package/components/ParqetCsvTableRenderer.svelte +9 -4
  7. package/package/components/Path.svelte +10 -0
  8. package/package/components/S3FilePickerInner.svelte +22 -8
  9. package/package/components/ScriptEditor.svelte +34 -4
  10. package/package/components/ShareModal.svelte.d.ts +1 -1
  11. package/package/components/VariableForm.svelte +1 -1
  12. package/package/components/apps/components/helpers/RunnableComponent.svelte.d.ts +1 -0
  13. package/package/components/common/fileDownload/FileDownload.svelte +16 -6
  14. package/package/components/flows/idUtils.js +4 -1
  15. package/package/components/flows/stepsInputArgs.svelte.js +6 -1
  16. package/package/components/instanceSettings/SecretBackendConfig.svelte +36 -11
  17. package/package/components/propertyPicker/ObjectViewer.svelte +10 -4
  18. package/package/components/runs/runsFilter.d.ts +1 -1
  19. package/package/components/runs/useJobsLoader.svelte.d.ts +1 -0
  20. package/package/components/runs/useJobsLoader.svelte.js +3 -1
  21. package/package/components/scriptEditor/LogPanel.svelte +4 -1
  22. package/package/components/scriptEditor/LogPanel.svelte.d.ts +1 -0
  23. package/package/components/settings/WorkspaceOperatorSettings.svelte +1 -1
  24. package/package/components/sidebar/SidebarContent.svelte +40 -2
  25. package/package/gen/core/OpenAPI.js +1 -1
  26. package/package/gen/schemas.gen.d.ts +33 -4
  27. package/package/gen/schemas.gen.js +33 -4
  28. package/package/gen/services.gen.d.ts +20 -1
  29. package/package/gen/services.gen.js +40 -0
  30. package/package/gen/types.gen.d.ts +182 -3
  31. package/package/system_prompts/prompts.d.ts +1 -1
  32. package/package/system_prompts/prompts.js +1 -1
  33. package/package/utils/downloadFile.d.ts +11 -0
  34. package/package/utils/downloadFile.js +48 -0
  35. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { Highlight } from 'svelte-highlight';
3
3
  import { json } from 'svelte-highlight/languages';
4
4
  import { copyToClipboard, parseS3Object, roughSizeOfObject } from '../utils';
5
5
  import { base } from '../base';
6
+ import { downloadViaClient, shouldDownloadViaClient } from '../utils/downloadFile';
6
7
  import { Button, Drawer, DrawerContent } from './common';
7
8
  import { ClipboardCopy, Download, PanelRightOpen, Table2, Braces, Highlighter, ArrowDownFromLine, Loader2 } from 'lucide-svelte';
8
9
  import Portal from './Portal.svelte';
@@ -72,6 +73,21 @@ function isTableRowObjectInner(json, hasHeaders) {
72
73
  }));
73
74
  }
74
75
  let largeObject = $state(undefined);
76
+ let resultApiPath = $derived(workspaceId && jobId
77
+ ? nodeId
78
+ ? `/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
79
+ : `/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
80
+ : undefined);
81
+ let resultDownloadHref = $derived(resultApiPath
82
+ ? `${base}/api${resultApiPath}`
83
+ : `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`);
84
+ let resultDownloadName = $derived(`${filename ?? 'result'}.json`);
85
+ async function onResultDownload(e) {
86
+ if (!resultApiPath || !shouldDownloadViaClient())
87
+ return;
88
+ e.preventDefault();
89
+ await downloadViaClient(resultApiPath, resultDownloadName);
90
+ }
75
91
  function checkIfS3(result, keys) {
76
92
  return keys.includes('s3') && typeof result.s3 === 'string';
77
93
  }
@@ -860,12 +876,9 @@ $effect(() => {
860
876
  {#if largeObject}
861
877
  <div class="text-xs text-emphasis"
862
878
  ><a
863
- download="{filename ?? 'result'}.json"
864
- href={workspaceId && jobId
865
- ? nodeId
866
- ? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
867
- : `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
868
- : `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
879
+ download={resultDownloadName}
880
+ href={resultDownloadHref}
881
+ onclick={onResultDownload}
869
882
  >
870
883
  Download {filename ? '' : 'as JSON'}
871
884
  </a>
@@ -933,19 +946,26 @@ $effect(() => {
933
946
  <DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
934
947
  {#snippet actions()}
935
948
  {#if customUi?.disableDownload !== true}
936
- <Button
937
- download="{filename ?? 'result'}.json"
938
- href={workspaceId && jobId
939
- ? nodeId
940
- ? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
941
- : `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
942
- : `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
943
- startIcon={{ icon: Download }}
944
- variant="subtle"
945
- unifiedSize="md"
946
- >
947
- Download
948
- </Button>
949
+ {#if resultApiPath && shouldDownloadViaClient()}
950
+ <Button
951
+ on:click={() => downloadViaClient(resultApiPath!, resultDownloadName)}
952
+ startIcon={{ icon: Download }}
953
+ variant="subtle"
954
+ unifiedSize="md"
955
+ >
956
+ Download
957
+ </Button>
958
+ {:else}
959
+ <Button
960
+ download={resultDownloadName}
961
+ href={resultDownloadHref}
962
+ startIcon={{ icon: Download }}
963
+ variant="subtle"
964
+ unifiedSize="md"
965
+ >
966
+ Download
967
+ </Button>
968
+ {/if}
949
969
  {/if}
950
970
  <Button
951
971
  on:click={() => copyToClipboard(toJsonStr(result))}
@@ -12,6 +12,7 @@ import Tabs from './common/tabs/Tabs.svelte';
12
12
  import {} from './graph';
13
13
  import ModuleStatus from './ModuleStatus.svelte';
14
14
  import { clone, isScriptPreview, msToSec, readFieldsRecursively, truncateRev } from '../utils';
15
+ import { downloadViaClient, shouldDownloadViaClient } from '../utils/downloadFile';
15
16
  import JobArgs from './JobArgs.svelte';
16
17
  import { ChevronDown, Download, ExternalLink, Hourglass } from 'lucide-svelte';
17
18
  import { deepEqual } from 'fast-equals';
@@ -1481,16 +1482,29 @@ let totalEventsWaiting = $derived(Object.values(suspendStatus?.val ?? {}).reduce
1481
1482
  style="min-height: {minTabHeight}px"
1482
1483
  >
1483
1484
  {#if !hideDownloadLogs && !isReplay && job?.id}
1485
+ {@const logsApiPath = `/w/${workspace}/jobs_u/get_flow_all_logs/${job.id}`}
1486
+ {@const logsName = `windmill_flow_logs_${job.id}.txt`}
1484
1487
  <div class="flex justify-end p-1">
1485
- <Button
1486
- href="{base}/api/w/{workspace}/jobs_u/get_flow_all_logs/{job.id}"
1487
- download="windmill_flow_logs_{job.id}.txt"
1488
- color="light"
1489
- size="xs"
1490
- startIcon={{ icon: Download }}
1491
- >
1492
- Download all logs
1493
- </Button>
1488
+ {#if shouldDownloadViaClient()}
1489
+ <Button
1490
+ on:click={() => downloadViaClient(logsApiPath, logsName)}
1491
+ color="light"
1492
+ size="xs"
1493
+ startIcon={{ icon: Download }}
1494
+ >
1495
+ Download all logs
1496
+ </Button>
1497
+ {:else}
1498
+ <Button
1499
+ href="{base}/api{logsApiPath}"
1500
+ download={logsName}
1501
+ color="light"
1502
+ size="xs"
1503
+ startIcon={{ icon: Download }}
1504
+ >
1505
+ Download all logs
1506
+ </Button>
1507
+ {/if}
1494
1508
  </div>
1495
1509
  {/if}
1496
1510
  <FlowLogViewerWrapper
@@ -81,7 +81,8 @@ let jobsLoader = useJobsLoader(() => ({
81
81
  syncQueuedRunsCount: false,
82
82
  refreshRate: 10000,
83
83
  currentWorkspace: $workspaceStore ?? '',
84
- skip: !runnableId
84
+ skip: !runnableId,
85
+ excludesEntrypointOverride: true
85
86
  }));
86
87
  let jobs = $derived(jobsLoader?.jobs ?? []);
87
88
  </script>
@@ -29,6 +29,7 @@ import SettingCard from './instanceSettings/SettingCard.svelte';
29
29
  let { setting, version, values, loading = true, openSmtpSettings, oauths, warning } = $props();
30
30
  const dispatch = createEventDispatcher();
31
31
  let latestKeyRenewalAttempt = $state(null);
32
+ let offlineCapStatus = $state(null);
32
33
  function showSetting(setting, values) {
33
34
  if (setting == 'dev_instance') {
34
35
  if (values['license_key'] == undefined) {
@@ -42,6 +43,14 @@ let opening = $state(false);
42
43
  async function reloadKeyrenewalAttemptInfo() {
43
44
  latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt();
44
45
  }
46
+ async function reloadLicenseStatus() {
47
+ try {
48
+ offlineCapStatus = (await SettingService.getOfflineLicenseStatus());
49
+ }
50
+ catch {
51
+ offlineCapStatus = null;
52
+ }
53
+ }
45
54
  async function reloadLicenseKey() {
46
55
  $values['license_key'] = await SettingService.getGlobal({
47
56
  key: 'license_key'
@@ -49,7 +58,10 @@ async function reloadLicenseKey() {
49
58
  }
50
59
  $effect(() => {
51
60
  if (setting.key == 'license_key') {
52
- untrack(() => reloadKeyrenewalAttemptInfo());
61
+ untrack(() => {
62
+ reloadKeyrenewalAttemptInfo();
63
+ reloadLicenseStatus();
64
+ });
53
65
  }
54
66
  });
55
67
  export async function renewLicenseKey() {
@@ -393,7 +405,7 @@ $effect(() => {
393
405
  </div>
394
406
  {/if}
395
407
  {/if}
396
- {#if latestKeyRenewalAttempt}
408
+ {#if latestKeyRenewalAttempt && !offlineCapStatus}
397
409
  {@const attemptedAt = new Date(latestKeyRenewalAttempt.attempted_at).toLocaleString()}
398
410
  {@const isTrial = latestKeyRenewalAttempt.result.startsWith('error: trial:')}
399
411
  <div class="relative">
@@ -463,11 +475,41 @@ $effect(() => {
463
475
  </div>
464
476
  {/if}
465
477
 
478
+ {#if offlineCapStatus}
479
+ {@const cap = offlineCapStatus}
480
+ {@const seatsOver = cap.seats_used > cap.seats_cap}
481
+ {@const cuOver = cap.cu_over_cap}
482
+ <div class="mt-1 flex flex-row items-center gap-2 text-xs">
483
+ <div class="flex flex-row items-center gap-1">
484
+ {#if seatsOver}
485
+ <BadgeX class="text-red-600" size={12} />
486
+ {:else}
487
+ <BadgeCheck class="text-green-600" size={12} />
488
+ {/if}
489
+ <span class={seatsOver ? 'text-red-600' : 'text-green-600'}>
490
+ Seats: {cap.seats_used.toFixed(1)} / {cap.seats_cap}
491
+ </span>
492
+ </div>
493
+ <div class="flex flex-row items-center gap-1">
494
+ {#if cuOver}
495
+ <BadgeX class="text-red-600" size={12} />
496
+ {:else}
497
+ <BadgeCheck class="text-green-600" size={12} />
498
+ {/if}
499
+ <span class={cuOver ? 'text-red-600' : 'text-green-600'}>
500
+ CUs: {cap.current_cu.toFixed(2)} / {cap.cu_cap.toFixed(2)}
501
+ </span>
502
+ </div>
503
+ </div>
504
+ {/if}
505
+
466
506
  {#if valid || expiration}
467
507
  <div class="flex flex-row gap-2 mt-1">
468
- <Button on:click={renewLicenseKey} loading={renewing} size="xs" variant="accent"
469
- >Renew key
470
- </Button>
508
+ {#if !offlineCapStatus}
509
+ <Button on:click={renewLicenseKey} loading={renewing} size="xs" variant="accent"
510
+ >Renew key
511
+ </Button>
512
+ {/if}
471
513
  <Button variant="accent" size="xs" loading={opening} on:click={openCustomerPortal}>
472
514
  Open customer portal
473
515
  </Button>
@@ -8,11 +8,12 @@ const s3LogPrefixes = [
8
8
  const S3_LOG_SEARCH_LIMIT = 2000;
9
9
  </script>
10
10
 
11
- <script lang="ts">import { ClipboardCopy, Download, Expand, Loader2 } from 'lucide-svelte';
11
+ <script lang="ts">import { ClipboardCopy, Download, Expand, Loader2, Timer, Cpu } from 'lucide-svelte';
12
12
  import { Button, Drawer, DrawerContent } from './common';
13
13
  import { copyToClipboard } from '../utils';
14
14
  import { base } from '../base';
15
15
  import { withExternalDomain } from '../externalDomain';
16
+ import { downloadViaClient, shouldDownloadViaClient } from '../utils/downloadFile';
16
17
  import { workspaceStore } from '../stores';
17
18
  import { AnsiUp } from 'ansi_up';
18
19
  import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte';
@@ -96,8 +97,7 @@ function truncateContent(jobContent, loadedFromObjectStore, limit) {
96
97
  return content;
97
98
  }
98
99
  export function scrollToBottom() {
99
- scroll &&
100
- setTimeout(() => preEl?.scroll({ top: preEl?.scrollHeight, behavior: 'smooth' }), 100);
100
+ scroll && setTimeout(() => preEl?.scroll({ top: preEl?.scrollHeight, behavior: 'smooth' }), 100);
101
101
  }
102
102
  let logViewer = $state();
103
103
  async function getStoreLogs() {
@@ -133,7 +133,15 @@ $effect.pre(() => {
133
133
  scroll = true;
134
134
  }
135
135
  });
136
- let downloadHref = $derived(withExternalDomain(`${base}/api/w/${$workspaceStore}/jobs_u/get_logs/${jobId}`));
136
+ let logsApiPath = $derived(`/w/${$workspaceStore}/jobs_u/get_logs/${jobId}`);
137
+ let downloadHref = $derived(withExternalDomain(`${base}/api${logsApiPath}`));
138
+ let downloadName = $derived(`windmill_logs_${jobId}.txt`);
139
+ async function onDownloadClick(e) {
140
+ if (!shouldDownloadViaClient())
141
+ return;
142
+ e.preventDefault();
143
+ await downloadViaClient(logsApiPath, downloadName);
144
+ }
137
145
  let truncatedContent = $derived(truncateContent(content, loadedFromObjectStore, LOG_LIMIT));
138
146
  let prefixInfo = $derived(findPrefixInfo(truncatedContent));
139
147
  let downloadStartUrl = $derived(findStartUrl(truncatedContent, prefixInfo));
@@ -171,17 +179,30 @@ let html = $derived.by(() => {
171
179
  <DrawerContent title="Expanded Logs" on:close={logViewer.closeDrawer}>
172
180
  {#snippet actions()}
173
181
  {#if jobId && download}
174
- <Button
175
- href={downloadHref}
176
- download="windmill_logs_{jobId}.txt"
177
- color="light"
178
- size="xs"
179
- startIcon={{
180
- icon: Download
181
- }}
182
- >
183
- Download
184
- </Button>
182
+ {#if shouldDownloadViaClient()}
183
+ <Button
184
+ on:click={() => downloadViaClient(logsApiPath, downloadName)}
185
+ color="light"
186
+ size="xs"
187
+ startIcon={{
188
+ icon: Download
189
+ }}
190
+ >
191
+ Download
192
+ </Button>
193
+ {:else}
194
+ <Button
195
+ href={downloadHref}
196
+ download={downloadName}
197
+ color="light"
198
+ size="xs"
199
+ startIcon={{
200
+ icon: Download
201
+ }}
202
+ >
203
+ Download
204
+ </Button>
205
+ {/if}
185
206
  {/if}
186
207
 
187
208
  <Button
@@ -220,7 +241,9 @@ let html = $derived.by(() => {
220
241
  class="w-full h-full bg-surface-secondary flex flex-col {noMaxH ? '' : 'max-h-screen'}"
221
242
  data-nav-id={navigationId}
222
243
  >
223
- <div class="flex gap-2 ml-2 {small ? 'py-1' : 'py-2'} border-b">
244
+ <div
245
+ class="flex gap-2 ml-2 {small ? 'py-1' : 'py-2'} border-b overflow-x-auto overflow-y-hidden"
246
+ >
224
247
  {#if isLoading}
225
248
  <div class="flex gap-2 items-center">
226
249
  <Loader2 class="animate-spin" />
@@ -236,14 +259,27 @@ let html = $derived.by(() => {
236
259
  </div>
237
260
  {:else if duration}
238
261
  <span
239
- class={twMerge('text-secondary dark:text-gray-400', small ? '!text-2xs' : '!text-xs')}
240
- >took {duration}ms</span
262
+ class={twMerge(
263
+ 'flex items-center gap-1 text-secondary dark:text-gray-400',
264
+ small ? '!text-2xs' : '!text-xs'
265
+ )}
266
+ title="Duration"
241
267
  >
268
+ <Timer size={small ? 10 : 12} />
269
+ {duration}ms
270
+ </span>
242
271
  {/if}
243
272
  {#if mem}
244
- <span class="{small ? '!text-2xs' : '!text-xs'} text-secondary dark:text-gray-400"
245
- >mem peak: {(mem / 1024).toPrecision(4)}MB</span
273
+ <span
274
+ class={twMerge(
275
+ 'flex items-center gap-1 text-secondary dark:text-gray-400',
276
+ small ? '!text-2xs' : '!text-xs'
277
+ )}
278
+ title="Memory peak"
246
279
  >
280
+ <Cpu size={small ? 10 : 12} />
281
+ {(mem / 1024).toPrecision(4)}MB
282
+ </span>
247
283
  {/if}
248
284
  <div class="flex gap-2 justify-end flex-1">
249
285
  {#if jobId && download}
@@ -252,15 +288,18 @@ let html = $derived.by(() => {
252
288
  class="text-primary pb-0.5"
253
289
  target="_blank"
254
290
  href={downloadHref}
255
- download="windmill_logs_{jobId}.txt"
291
+ download={downloadName}
292
+ onclick={onDownloadClick}
256
293
  ><Download size="14" />
257
294
  </a>
258
295
  </div>
259
296
  {/if}
260
297
  <button onclick={logViewer.openDrawer}><Expand size="12" /></button>
261
298
  {#if !noAutoScroll}
262
- <label class="pr-2 text-2xs flex gap-2 font-normal text-primary items-center">
263
- Auto scroll
299
+ <label
300
+ class="pr-2 text-2xs flex gap-2 font-normal text-primary items-center whitespace-nowrap"
301
+ >
302
+ auto-scroll
264
303
  <input class="windmillapp" type="checkbox" bind:checked={scroll} />
265
304
  </label>
266
305
  {/if}
@@ -6,6 +6,7 @@ import { twMerge } from 'tailwind-merge';
6
6
  import DarkModeObserver from './DarkModeObserver.svelte';
7
7
  import { HelpersService } from '../gen';
8
8
  import { base } from '../base';
9
+ import { downloadViaClient, shouldDownloadViaClient } from '../utils/downloadFile';
9
10
  import { enterpriseLicense, workspaceStore } from '../stores';
10
11
  import { Download } from 'lucide-svelte';
11
12
  import { Loader2 } from 'lucide-svelte';
@@ -171,13 +172,17 @@ run(() => {
171
172
  </div>
172
173
  {/if}
173
174
  {#if !disable_download && !s3resource.endsWith('.csv')}
175
+ {@const csvApiPath = `/w/${workspaceId}/job_helpers/download_s3_parquet_file_as_csv?file_key=${encodeURIComponent(s3resource)}${storage ? `&storage=${storage}` : ''}`}
176
+ {@const csvName = (s3resource.split('/').pop() ?? 'download') + '.csv'}
174
177
  <a
175
178
  target="_blank"
176
- href="{base}/api/w/{workspaceId}/job_helpers/download_s3_parquet_file_as_csv?file_key={encodeURIComponent(
177
- s3resource
178
- )}{storage ? `&storage=${storage}` : ''}"
179
+ href="{base}/api{csvApiPath}"
179
180
  class="text-secondary w-full text-right underline text-2xs whitespace-nowrap"
180
- ><div class="flex flex-row-reverse gap-2 items-center"><Download size={12} /> CSV</div></a
181
+ onclick={async (e) => {
182
+ if (!shouldDownloadViaClient()) return
183
+ e.preventDefault()
184
+ await downloadViaClient(csvApiPath, csvName)
185
+ }}><div class="flex flex-row-reverse gap-2 items-center"><Download size={12} /> CSV</div></a
181
186
  >
182
187
  {/if}
183
188
 
@@ -282,6 +282,16 @@ async function initPath() {
282
282
  function setDirty() {
283
283
  !dirty && (dirty = true);
284
284
  }
285
+ $effect(() => {
286
+ if (path !== undefined &&
287
+ path !== '' &&
288
+ initialPath &&
289
+ !initialPath.startsWith('tmp/') &&
290
+ path !== initialPath &&
291
+ !dirty) {
292
+ dirty = true;
293
+ }
294
+ });
285
295
  const openSearchWithPrefilledText = getContext('openSearchWithPrefilledText');
286
296
  $effect.pre(() => {
287
297
  ;
@@ -5,6 +5,7 @@ import { workspaceStore } from '../stores';
5
5
  import { CancelablePromise, HelpersService } from '../gen';
6
6
  import { base } from '../base';
7
7
  import { displayDate, displaySize, emptyString, parseS3Object, sendUserToast } from '../utils';
8
+ import { downloadViaClient, shouldDownloadViaClient } from '../utils/downloadFile';
8
9
  import { Alert, Button } from './common';
9
10
  import Section from './Section.svelte';
10
11
  import { createEventDispatcher, untrack } from 'svelte';
@@ -577,14 +578,27 @@ $effect.pre(() => {
577
578
  {#if filePreview !== undefined && (!hideS3SpecificDetails || !readOnlyMode || allowDelete)}
578
579
  <div class="flex gap-2 shrink-0">
579
580
  {#if !hideS3SpecificDetails}
580
- <Button
581
- title="Download file from S3"
582
- variant="default"
583
- href={`${base}/api/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}`}
584
- download={fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
585
- startIcon={{ icon: Download }}
586
- iconOnly={true}
587
- />
581
+ {@const downloadApiPath = `/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}`}
582
+ {@const downloadName =
583
+ fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
584
+ {#if shouldDownloadViaClient()}
585
+ <Button
586
+ title="Download file from S3"
587
+ variant="default"
588
+ on:click={() => downloadViaClient(downloadApiPath, downloadName)}
589
+ startIcon={{ icon: Download }}
590
+ iconOnly={true}
591
+ />
592
+ {:else}
593
+ <Button
594
+ title="Download file from S3"
595
+ variant="default"
596
+ href={`${base}/api${downloadApiPath}`}
597
+ download={downloadName}
598
+ startIcon={{ icon: Download }}
599
+ iconOnly={true}
600
+ />
601
+ {/if}
588
602
  {/if}
589
603
  {#if !readOnlyMode}
590
604
  <Button
@@ -408,6 +408,8 @@ let logPanel = $state(undefined);
408
408
  let testIsLoading = $state(false);
409
409
  let testJob = $state();
410
410
  let pastPreviews = $state([]);
411
+ let historyTabActive = false;
412
+ let pastPreviewsRequest;
411
413
  let validCode = $state(true);
412
414
  // Recording
413
415
  let scriptRecording = createScriptRecording();
@@ -511,7 +513,9 @@ export async function runTest() {
511
513
  lastRecording = scriptRecording.stop();
512
514
  setActiveRecording(undefined);
513
515
  }
514
- loadPastTests();
516
+ if (historyTabActive) {
517
+ loadPastTests();
518
+ }
515
519
  },
516
520
  doneError({ error }) {
517
521
  if (scriptRecording.active) {
@@ -536,12 +540,31 @@ function downloadRecording() {
536
540
  }
537
541
  }
538
542
  async function loadPastTests() {
539
- pastPreviews = await JobService.listCompletedJobs({
543
+ pastPreviewsRequest?.cancel();
544
+ const req = JobService.listCompletedJobs({
540
545
  workspace: $workspaceStore,
541
546
  jobKinds: 'preview',
542
547
  createdBy: $userStore?.username,
543
- scriptPathExact: path
548
+ scriptPathExact: path,
549
+ hasNullParent: true
544
550
  });
551
+ pastPreviewsRequest = req;
552
+ try {
553
+ const result = await req;
554
+ if (pastPreviewsRequest === req) {
555
+ pastPreviews = result;
556
+ }
557
+ }
558
+ catch (err) {
559
+ if (!(err instanceof Error) || err.name !== 'CancelError') {
560
+ throw err;
561
+ }
562
+ }
563
+ finally {
564
+ if (pastPreviewsRequest === req) {
565
+ pastPreviewsRequest = undefined;
566
+ }
567
+ }
545
568
  }
546
569
  export async function inferSchema(code, { nlang, resetArgs = false, applyInitialArgs = false } = {}) {
547
570
  let nschema = schema ?? emptySchema();
@@ -897,7 +920,6 @@ onMount(async () => {
897
920
  if (!validCode && code && lang) {
898
921
  await inferSchema(code, { applyInitialArgs: true });
899
922
  }
900
- loadPastTests();
901
923
  aiChatManager.saveAndClear();
902
924
  aiChatManager.changeMode(AIMode.SCRIPT);
903
925
  });
@@ -964,6 +986,8 @@ export function disableCollaboration() {
964
986
  wsProvider = undefined;
965
987
  }
966
988
  onDestroy(() => {
989
+ pastPreviewsRequest?.cancel();
990
+ pastPreviewsRequest = undefined;
967
991
  disableCollaboration();
968
992
  aiChatManager.scriptEditorApplyCode = undefined;
969
993
  aiChatManager.scriptEditorShowDiffMode = undefined;
@@ -1413,6 +1437,12 @@ $effect(() => {
1413
1437
  } as any)
1414
1438
  : testJob}
1415
1439
  {pastPreviews}
1440
+ onTabChange={(tab) => {
1441
+ historyTabActive = tab === 'history'
1442
+ if (historyTabActive) {
1443
+ loadPastTests()
1444
+ }
1445
+ }}
1416
1446
  previewIsLoading={debugMode
1417
1447
  ? $debugState.running && !$debugState.stopped
1418
1448
  : testIsLoading}
@@ -16,7 +16,7 @@ declare const ShareModal: $$__sveltets_2_IsomorphicComponent<Record<string, neve
16
16
  } & {
17
17
  [evt: string]: CustomEvent<any>;
18
18
  }, {}, {
19
- openDrawer: (newPath: string, kind_l: "resource" | "volume" | "script" | "flow" | "app" | "variable" | "schedule" | "raw_app" | "group_" | "http_trigger" | "websocket_trigger" | "kafka_trigger" | "nats_trigger" | "postgres_trigger" | "mqtt_trigger" | "gcp_trigger" | "azure_trigger" | "sqs_trigger" | "email_trigger", isOwnerOverride?: boolean) => Promise<void>;
19
+ openDrawer: (newPath: string, kind_l: "resource" | "volume" | "script" | "flow" | "app" | "variable" | "schedule" | "raw_app" | "http_trigger" | "websocket_trigger" | "kafka_trigger" | "nats_trigger" | "postgres_trigger" | "mqtt_trigger" | "sqs_trigger" | "gcp_trigger" | "azure_trigger" | "email_trigger" | "group_", isOwnerOverride?: boolean) => Promise<void>;
20
20
  }, "">;
21
21
  type ShareModal = InstanceType<typeof ShareModal>;
22
22
  export default ShareModal;
@@ -51,7 +51,7 @@ export function setCode(value) {
51
51
  {#if deployTo}
52
52
  <Label
53
53
  label="Workspace specific"
54
- tooltip="Prevents this variable from being deployed to prod/staging"
54
+ tooltip="Prevents this variable from being deployed to prod/staging. May have been enabled automatically because a workspace-specific resource references this variable via $var:. Disabling this toggle does not retroactively un-mark the resource that referenced it."
55
55
  >
56
56
  <Toggle bind:checked={wsSpecific} />
57
57
  </Label>
@@ -84,6 +84,7 @@ declare const RunnableComponent: $$__sveltets_2_IsomorphicComponent<Props, {
84
84
  path?: string;
85
85
  lock?: string;
86
86
  cache_ttl?: number;
87
+ tag?: string;
87
88
  };
88
89
  id?: number;
89
90
  force_viewer_static_fields?: {
@@ -1,7 +1,20 @@
1
1
  <script lang="ts">import { workspaceStore } from '../../../stores';
2
2
  import { Download } from 'lucide-svelte';
3
3
  import { base } from '../../../base';
4
+ import { downloadViaClient, shouldDownloadViaClient } from '../../../utils/downloadFile';
4
5
  let { s3object, workspaceId = undefined, appPath = undefined } = $props();
6
+ let workspace = $derived(workspaceId ?? $workspaceStore);
7
+ let filename = $derived(s3object?.s3?.split?.('/')?.pop() ?? 'unnamed_download.file');
8
+ let apiPath = $derived(`${appPath
9
+ ? `/w/${workspace}/apps_u/download_s3_file/${appPath}`
10
+ : `/w/${workspace}/job_helpers/download_s3_file`}?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(s3object?.s3 ?? '')}${s3object?.storage ? `&storage=${s3object.storage}` : ''}${appPath && s3object?.presigned ? `&${s3object?.presigned}` : ''}`);
11
+ let href = $derived(`${base}/api${apiPath}`);
12
+ async function onclick(e) {
13
+ if (!shouldDownloadViaClient())
14
+ return;
15
+ e.preventDefault();
16
+ await downloadViaClient(apiPath, filename);
17
+ }
5
18
  </script>
6
19
 
7
20
  {#if s3object && s3object?.s3}
@@ -10,12 +23,9 @@ let { s3object, workspaceId = undefined, appPath = undefined } = $props();
10
23
  border border-dashed border-gray-400 hover:border-blue-500
11
24
  focus-within:border-blue-500 hover:bg-blue-50 dark:hover:bg-frost-900 focus-within:bg-blue-50
12
25
  duration-200 rounded-lg p-1 gap-2"
13
- href={`${base}/api/w/${workspaceId ?? $workspaceStore}${
14
- appPath ? `/apps_u/download_s3_file/${appPath}` : '/job_helpers/download_s3_file'
15
- }?${appPath ? 's3' : 'file_key'}=${encodeURIComponent(s3object?.s3 ?? '')}${
16
- s3object?.storage ? `&storage=${s3object.storage}` : ''
17
- }${appPath && s3object?.presigned ? `&${s3object?.presigned}` : ''}`}
18
- download={s3object?.s3?.split?.('/')?.pop() ?? 'unnamed_download.file'}
26
+ {href}
27
+ download={filename}
28
+ {onclick}
19
29
  >
20
30
  <Download />
21
31
  <span>
@@ -13,7 +13,10 @@ export const forbiddenIds = [
13
13
  'in',
14
14
  'failure',
15
15
  'preprocessor',
16
- 'as'
16
+ 'as',
17
+ 'Input',
18
+ 'Result',
19
+ 'Trigger'
17
20
  ];
18
21
  export function numberToChars(n) {
19
22
  if (n < 0) {
@@ -1,4 +1,4 @@
1
- import { dfs, getPreviousModule, getStepPropPicker } from './previousResults';
1
+ import { dfs, getPreviousModule, getStepPropPicker, getFailureStepPropPicker } from './previousResults';
2
2
  import { evalValue } from './utils.svelte';
3
3
  export class StepsInputArgs {
4
4
  #stepsEvaluated = $state({});
@@ -103,6 +103,11 @@ export class StepsInputArgs {
103
103
  this.#steps[mod.id] = argsSnapshot;
104
104
  }
105
105
  updateStepArgs(id, flowState, flow, previewArgs) {
106
+ if (id === 'failure' && flow && flow.value.failure_module && flowState) {
107
+ const picker = getFailureStepPropPicker(flowState, flow, previewArgs);
108
+ this.initializeFromSchema(flow.value.failure_module, flowState['failure']?.schema ?? {}, picker.pickableProperties);
109
+ return;
110
+ }
106
111
  if (!flowState || !flow) {
107
112
  return;
108
113
  }