windmill-components 1.698.0 → 1.700.1

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 (46) hide show
  1. package/package/components/Auth0Setting.svelte +7 -5
  2. package/package/components/AuthSettings.svelte +11 -6
  3. package/package/components/AutheliaSetting.svelte +6 -4
  4. package/package/components/AuthentikSetting.svelte +7 -2
  5. package/package/components/DisplayResult.svelte +41 -23
  6. package/package/components/FlowStatusViewerInner.svelte +23 -9
  7. package/package/components/HistoricInputs.svelte +2 -1
  8. package/package/components/InstanceSetting.svelte +47 -5
  9. package/package/components/KanidmSetting.svelte +6 -4
  10. package/package/components/KeycloakSetting.svelte +6 -4
  11. package/package/components/LogViewer.svelte +69 -29
  12. package/package/components/NextcloudSetting.svelte +6 -4
  13. package/package/components/OAuthSetting.svelte +6 -4
  14. package/package/components/OktaSetting.svelte +7 -2
  15. package/package/components/ParqetCsvTableRenderer.svelte +21 -8
  16. package/package/components/Path.svelte +10 -0
  17. package/package/components/PocketIdSetting.svelte +6 -4
  18. package/package/components/S3FilePickerInner.svelte +22 -8
  19. package/package/components/ScriptEditor.svelte +34 -4
  20. package/package/components/ShareModal.svelte.d.ts +1 -1
  21. package/package/components/VariableForm.svelte +1 -1
  22. package/package/components/ZitadelSetting.svelte +6 -4
  23. package/package/components/apps/components/helpers/RunnableComponent.svelte.d.ts +1 -0
  24. package/package/components/common/fileDownload/FileDownload.svelte +26 -17
  25. package/package/components/flows/idUtils.js +4 -1
  26. package/package/components/flows/stepsInputArgs.svelte.js +6 -1
  27. package/package/components/instanceSettings/SecretBackendConfig.svelte +36 -11
  28. package/package/components/propertyPicker/ObjectViewer.svelte +23 -9
  29. package/package/components/runs/runsFilter.d.ts +1 -1
  30. package/package/components/runs/useJobsLoader.svelte.d.ts +1 -0
  31. package/package/components/runs/useJobsLoader.svelte.js +3 -1
  32. package/package/components/scriptEditor/LogPanel.svelte +4 -1
  33. package/package/components/scriptEditor/LogPanel.svelte.d.ts +1 -0
  34. package/package/components/settings/WorkspaceOperatorSettings.svelte +1 -1
  35. package/package/components/sidebar/SidebarContent.svelte +40 -2
  36. package/package/gen/core/OpenAPI.js +1 -1
  37. package/package/gen/schemas.gen.d.ts +33 -4
  38. package/package/gen/schemas.gen.js +33 -4
  39. package/package/gen/services.gen.d.ts +20 -1
  40. package/package/gen/services.gen.js +40 -0
  41. package/package/gen/types.gen.d.ts +182 -3
  42. package/package/system_prompts/prompts.d.ts +2 -2
  43. package/package/system_prompts/prompts.js +2 -2
  44. package/package/utils/downloadFile.d.ts +11 -0
  45. package/package/utils/downloadFile.js +48 -0
  46. package/package.json +1 -1
@@ -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,14 +172,26 @@ run(() => {
171
172
  </div>
172
173
  {/if}
173
174
  {#if !disable_download && !s3resource.endsWith('.csv')}
174
- <a
175
- 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
- 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
- >
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'}
177
+ {#if shouldDownloadViaClient()}
178
+ <button
179
+ class="text-secondary w-full text-right underline text-2xs whitespace-nowrap"
180
+ onclick={() => downloadViaClient(csvApiPath, csvName)}
181
+ ><div class="flex flex-row-reverse gap-2 items-center"
182
+ ><Download size={12} /> CSV</div
183
+ ></button
184
+ >
185
+ {:else}
186
+ <a
187
+ target="_blank"
188
+ href="{base}/api{csvApiPath}"
189
+ class="text-secondary w-full text-right underline text-2xs whitespace-nowrap"
190
+ ><div class="flex flex-row-reverse gap-2 items-center"
191
+ ><Download size={12} /> CSV</div
192
+ ></a
193
+ >
194
+ {/if}
182
195
  {/if}
183
196
 
184
197
  {#if nbRows != undefined}
@@ -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
  ;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">import { untrack } from 'svelte';
2
2
  import IconedResourceType from './IconedResourceType.svelte';
3
3
  import TextInput from './text_input/TextInput.svelte';
4
+ import Password from './Password.svelte';
4
5
  import Toggle from './Toggle.svelte';
5
6
  import SettingCard from './instanceSettings/SettingCard.svelte';
6
7
  let { value = $bindable() } = $props();
@@ -71,11 +72,12 @@ function handleToggle(e) {
71
72
  bind:value={value['id']}
72
73
  />
73
74
  </label>
74
- <label class="flex flex-col gap-1">
75
+ <label for="pocketid_client_secret" class="flex flex-col gap-1">
75
76
  <span class="text-emphasis font-semibold text-xs">Client Secret</span>
76
- <TextInput
77
- inputProps={{ type: 'text', placeholder: 'Client Secret' }}
78
- bind:value={value['secret']}
77
+ <Password
78
+ id="pocketid_client_secret"
79
+ placeholder="Client Secret"
80
+ bind:password={value['secret']}
79
81
  />
80
82
  </label>
81
83
  </SettingCard>
@@ -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>
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">import { run } from 'svelte/legacy';
2
2
  import IconedResourceType from './IconedResourceType.svelte';
3
3
  import TextInput from './text_input/TextInput.svelte';
4
+ import Password from './Password.svelte';
4
5
  import Toggle from './Toggle.svelte';
5
6
  import SettingCard from './instanceSettings/SettingCard.svelte';
6
7
  let { value = $bindable() } = $props();
@@ -64,11 +65,12 @@ run(() => {
64
65
  bind:value={value['id']}
65
66
  />
66
67
  </label>
67
- <label>
68
+ <label for="zitadel_client_secret">
68
69
  <span class="text-emphasis font-semibold text-xs">Client Secret </span>
69
- <TextInput
70
- inputProps={{ type: 'text', placeholder: 'Client Secret' }}
71
- bind:value={value['secret']}
70
+ <Password
71
+ id="zitadel_client_secret"
72
+ placeholder="Client Secret"
73
+ bind:password={value['secret']}
72
74
  />
73
75
  </label>
74
76
  </SettingCard>
@@ -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,25 +1,34 @@
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
+ const sharedClass = `relative center-center flex w-full text-center font-normal text-primary text-sm
13
+ border border-dashed border-gray-400 hover:border-blue-500
14
+ focus-within:border-blue-500 hover:bg-blue-50 dark:hover:bg-frost-900 focus-within:bg-blue-50
15
+ duration-200 rounded-lg p-1 gap-2`;
5
16
  </script>
6
17
 
7
18
  {#if s3object && s3object?.s3}
8
- <a
9
- class="relative center-center flex w-full text-center font-normal text-primary text-sm
10
- border border-dashed border-gray-400 hover:border-blue-500
11
- focus-within:border-blue-500 hover:bg-blue-50 dark:hover:bg-frost-900 focus-within:bg-blue-50
12
- 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'}
19
- >
20
- <Download />
21
- <span>
22
- {s3object?.storage ? `s3://${s3object.storage}/${s3object.s3}` : `s3:///${s3object.s3}`}
23
- </span>
24
- </a>
19
+ {#if shouldDownloadViaClient()}
20
+ <button class={sharedClass} onclick={() => downloadViaClient(apiPath, filename)}>
21
+ <Download />
22
+ <span>
23
+ {s3object?.storage ? `s3://${s3object.storage}/${s3object.s3}` : `s3:///${s3object.s3}`}
24
+ </span>
25
+ </button>
26
+ {:else}
27
+ <a class={sharedClass} {href} download={filename}>
28
+ <Download />
29
+ <span>
30
+ {s3object?.storage ? `s3://${s3object.storage}/${s3object.s3}` : `s3:///${s3object.s3}`}
31
+ </span>
32
+ </a>
33
+ {/if}
25
34
  {/if}
@@ -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
  }
@@ -54,6 +54,7 @@ function setBackendType(type) {
54
54
  address: $values['secret_backend']?.address ?? '',
55
55
  mount_path: $values['secret_backend']?.mount_path ?? 'windmill',
56
56
  jwt_role: $values['secret_backend']?.jwt_role ?? 'windmill-secrets',
57
+ jwt_mount_path: $values['secret_backend']?.jwt_mount_path ?? null,
57
58
  namespace: $values['secret_backend']?.namespace ?? null,
58
59
  token: $values['secret_backend']?.token ?? null,
59
60
  skip_ssl_verify: $values['secret_backend']?.skip_ssl_verify ?? false
@@ -105,6 +106,7 @@ function getVaultSettings() {
105
106
  address: $values['secret_backend'].address,
106
107
  mount_path: $values['secret_backend'].mount_path,
107
108
  jwt_role: $values['secret_backend'].jwt_role,
109
+ jwt_mount_path: $values['secret_backend'].jwt_mount_path || undefined,
108
110
  namespace: $values['secret_backend'].namespace || undefined,
109
111
  token: $values['secret_backend'].token || undefined,
110
112
  skip_ssl_verify: $values['secret_backend'].skip_ssl_verify || undefined
@@ -317,6 +319,8 @@ function isAwsSmConfigValid() {
317
319
  return $values['secret_backend'].region?.trim() !== '';
318
320
  }
319
321
  let baseUrl = $derived($values['base_url'] ?? 'https://your-windmill-instance.com');
322
+ let jwtMount = $derived(($values['secret_backend']?.jwt_mount_path?.trim() || 'jwt'));
323
+ let vaultAudience = $derived(($values['secret_backend']?.address?.trim() || 'https://vault.example.com:8200'));
320
324
  </script>
321
325
 
322
326
  <div class="space-y-6">
@@ -465,6 +469,24 @@ let baseUrl = $derived($values['base_url'] ?? 'https://your-windmill-instance.co
465
469
  }}
466
470
  bind:value={$values['secret_backend'].jwt_role}
467
471
  />
472
+ <label for="vault_jwt_mount_path" class="block text-xs font-semibold text-emphasis"
473
+ >JWT Auth Mount Path (optional)</label
474
+ >
475
+ <span class="text-2xs text-secondary"
476
+ >Mount path of the JWT auth method in Vault. Defaults to <code>jwt</code>. Set this
477
+ only if you mounted the JWT auth method at a non-default path (<code
478
+ >vault auth enable -path=&lt;mount&gt; jwt</code
479
+ >).</span
480
+ >
481
+ <TextInput
482
+ inputProps={{
483
+ type: 'text',
484
+ id: 'vault_jwt_mount_path',
485
+ placeholder: 'jwt',
486
+ disabled
487
+ }}
488
+ bind:value={$values['secret_backend'].jwt_mount_path}
489
+ />
468
490
  <details class="mt-2">
469
491
  <summary class="text-xs font-medium text-secondary cursor-pointer hover:text-primary"
470
492
  >Vault JWT Setup Instructions</summary
@@ -475,29 +497,32 @@ let baseUrl = $derived($values['base_url'] ?? 'https://your-windmill-instance.co
475
497
  class="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-2xs overflow-x-auto"
476
498
  >
477
499
  <pre
478
- ># Enable JWT auth method
479
- vault auth enable jwt
500
+ ># Enable JWT auth method{jwtMount === 'jwt'
501
+ ? ''
502
+ : ` at custom mount '${jwtMount}'`}
503
+ vault auth enable {jwtMount === 'jwt' ? 'jwt' : `-path=${jwtMount} jwt`}
480
504
 
481
505
  # Configure JWT auth with Windmill's JWKS endpoint
482
- vault write auth/jwt/config \
483
- jwks_url="{baseUrl}/.well-known/jwks.json" \
484
- bound_issuer="{baseUrl}"
506
+ vault write auth/{jwtMount}/config \
507
+ jwks_url="{baseUrl}/api/oidc/jwks" \
508
+ bound_issuer="{baseUrl}/api/oidc/"
485
509
 
486
510
  # Create a policy for Windmill secrets
487
511
  vault policy write windmill-secrets - &lt;&lt;EOF
488
- path "windmill/data/*" &#123;
512
+ path "{$values['secret_backend']?.mount_path ?? 'windmill'}/data/*" &#123;
489
513
  capabilities = ["create", "read", "update", "delete"]
490
514
  &#125;
491
- path "windmill/metadata/*" &#123;
515
+ path "{$values['secret_backend']?.mount_path ?? 'windmill'}/metadata/*" &#123;
492
516
  capabilities = ["list", "delete"]
493
517
  &#125;
494
518
  EOF
495
519
 
496
- # Create the JWT role
497
- vault write auth/jwt/role/windmill-secrets \
520
+ # Create the JWT role. bound_audiences must match the Vault server
521
+ # address Windmill signs the JWT with `aud` = your Vault address.
522
+ vault write auth/{jwtMount}/role/{$values['secret_backend']?.jwt_role || 'windmill-secrets'} \
498
523
  role_type="jwt" \
499
- bound_audiences="{baseUrl}" \
500
- user_claim="email" \
524
+ bound_audiences="{vaultAudience}" \
525
+ user_claim="sub" \
501
526
  policies="windmill-secrets" \
502
527
  ttl="1h"</pre
503
528
  >
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">import ObjectViewer from './ObjectViewer.svelte';
2
2
  import { copyToClipboard, truncate } from '../../utils';
3
+ import { downloadViaClient, shouldDownloadViaClient } from '../../utils/downloadFile';
3
4
  import { createEventDispatcher, tick, untrack, setContext, getContext } from 'svelte';
4
5
  import { computeKey, keepByKeyOrValue } from './utils';
5
6
  import { NEVER_TESTED_THIS_FAR } from '../flows/models';
@@ -335,15 +336,28 @@ let menuItems = $derived([
335
336
  <div class="flex">
336
337
  <span class="text-primary">{closeBracket}</span>
337
338
  {#if getTypeAsString(jsonFiltered) === 's3object'}
338
- <a
339
- class="text-secondary underline font-semibold text-2xs whitespace-nowrap ml-1 w-fit"
340
- href={`/api/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(
341
- jsonFiltered?.s3 ?? ''
342
- )}${jsonFiltered?.storage ? `&storage=${jsonFiltered.storage}` : ''}`}
343
- download={jsonFiltered?.s3.split('/').pop() ?? 'unnamed_download.file'}
344
- >
345
- <span class="flex items-center gap-1"><Download size={12} />download</span>
346
- </a>
339
+ {@const s3DownloadApiPath = `/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(jsonFiltered?.s3 ?? '')}${jsonFiltered?.storage ? `&storage=${jsonFiltered.storage}` : ''}`}
340
+ {@const s3DownloadName = jsonFiltered?.s3.split('/').pop() ?? 'unnamed_download.file'}
341
+ {#if shouldDownloadViaClient()}
342
+ <button
343
+ class="text-secondary underline font-semibold text-2xs whitespace-nowrap ml-1 w-fit"
344
+ onclick={() => downloadViaClient(s3DownloadApiPath, s3DownloadName)}
345
+ >
346
+ <span class="flex items-center gap-1"
347
+ ><Download size={12} />download</span
348
+ >
349
+ </button>
350
+ {:else}
351
+ <a
352
+ class="text-secondary underline font-semibold text-2xs whitespace-nowrap ml-1 w-fit"
353
+ href={`/api${s3DownloadApiPath}`}
354
+ download={s3DownloadName}
355
+ >
356
+ <span class="flex items-center gap-1"
357
+ ><Download size={12} />download</span
358
+ >
359
+ </a>
360
+ {/if}
347
361
  <button
348
362
  class="text-secondary underline text-2xs whitespace-nowrap ml-1"
349
363
  onclick={() => {
@@ -178,7 +178,7 @@ export declare function buildRunsFilterSearchbarSchema({ paths, usernames, folde
178
178
  label: string;
179
179
  icon: typeof Zap;
180
180
  options: {
181
- label: "Google" | "GitHub" | "Schedule" | "HTTP" | "WebSocket" | "Postgres" | "Kafka" | "NATS" | "MQTT" | "SQS" | "GCP Pub/Sub" | "Azure Event Grid" | "Email" | "Webhook" | "Default Email";
181
+ label: any;
182
182
  value: JobTriggerKind;
183
183
  }[];
184
184
  allowNegative: true;
@@ -16,6 +16,7 @@ export interface UseJobLoaderArgs {
16
16
  skip?: boolean;
17
17
  lookback?: number;
18
18
  perPage?: number;
19
+ excludesEntrypointOverride?: boolean;
19
20
  }
20
21
  export declare function useJobsLoader(args: () => UseJobLoaderArgs): {
21
22
  loadExtraJobs: () => Promise<void>;
@@ -43,6 +43,7 @@ export function useJobsLoader(args) {
43
43
  let lookback = $derived(_args.lookback ?? 0);
44
44
  let timeframe = $derived(_args?.timeframe);
45
45
  let perPage = $derived(_args?.perPage ?? 1000);
46
+ let excludesEntrypointOverride = $derived(_args.excludesEntrypointOverride ?? false);
46
47
  let label = $derived(filters?.label ?? null);
47
48
  let worker = $derived(filters?.worker ?? null);
48
49
  let success = $derived(filters?.status ?? null);
@@ -212,7 +213,8 @@ export function useJobsLoader(args) {
212
213
  allWorkspaces: allWorkspaces ? true : undefined,
213
214
  perPage: perPageOverride ?? perPage,
214
215
  allowWildcards: allowWildcards ? true : undefined,
215
- broadFilter
216
+ broadFilter,
217
+ excludesEntrypointOverride: excludesEntrypointOverride ? true : undefined
216
218
  });
217
219
  promise = CancelablePromiseUtils.catchErr(promise, (e) => {
218
220
  if (e instanceof CancelError)
@@ -21,10 +21,13 @@ import Head from '../table/Head.svelte';
21
21
  import WorkflowTimeline from '../WorkflowTimeline.svelte';
22
22
  import Tooltip from '../Tooltip.svelte';
23
23
  import { getStringError } from '../copilot/chat/utils';
24
- let { lang, previewIsLoading = false, previewJob, pastPreviews = [], editor = undefined, diffEditor = undefined, args = undefined, workspace = undefined, showCaptures = false, customUi = undefined, children, capturesTab, customResultPanel, showCustomResultPanel = false } = $props();
24
+ let { lang, previewIsLoading = false, previewJob, pastPreviews = [], editor = undefined, diffEditor = undefined, args = undefined, workspace = undefined, showCaptures = false, customUi = undefined, children, capturesTab, customResultPanel, showCustomResultPanel = false, onTabChange } = $props();
25
25
  let selectedTab = $state('logs');
26
26
  let drawerOpen = $state(false);
27
27
  let drawerContent = $state(undefined);
28
+ $effect(() => {
29
+ onTabChange?.(selectedTab);
30
+ });
28
31
  export function setFocusToLogs() {
29
32
  selectedTab = 'logs';
30
33
  }
@@ -21,6 +21,7 @@ interface Props {
21
21
  capturesTab?: import('svelte').Snippet;
22
22
  customResultPanel?: import('svelte').Snippet;
23
23
  showCustomResultPanel?: boolean;
24
+ onTabChange?: (tab: string) => void;
24
25
  }
25
26
  declare const LogPanel: import("svelte").Component<Props, {
26
27
  setFocusToLogs: () => void;
@@ -15,7 +15,7 @@ let operatorWorkspaceSettings = $state({
15
15
  schedules: true,
16
16
  resources: true,
17
17
  variables: true,
18
- assets: false,
18
+ assets: true,
19
19
  triggers: true,
20
20
  audit_logs: true,
21
21
  groups: true,
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">import MenuLink from './MenuLink.svelte';
2
- import { superadmin, usedTriggerKinds, userStore, workspaceStore, isCriticalAlertsUIOpen, enterpriseLicense, devopsRole, tutorialsToDo, skippedAll } from '../../stores';
2
+ import { superadmin, usedTriggerKinds, userStore, userWorkspaces, workspaceStore, isCriticalAlertsUIOpen, enterpriseLicense, devopsRole, tutorialsToDo, skippedAll } from '../../stores';
3
+ import { findWorkspaceDescendants } from '../../utils/workspaceHierarchy';
3
4
  import { syncTutorialsTodos } from '../../tutorialUtils';
4
5
  import { SIDEBAR_SHOW_SCHEDULES } from '../../consts';
5
6
  import { BookOpen, ServerCog, Boxes, Calendar, DollarSign, Eye, Logs, FolderCog, FolderOpen, Github, GraduationCap, HelpCircle, Home, LogOut, Newspaper, Play, Route, Settings, UserCog, Plus, Unplug, AlertCircle, Database, Pyramid, Trash2, MailIcon } from 'lucide-svelte';
@@ -64,11 +65,24 @@ async function deleteFork() {
64
65
  sendUserToast(err, true);
65
66
  }
66
67
  }
68
+ if (deleteForkedChildren && forkedDescendants.length > 0) {
69
+ for (const child of forkedDescendants) {
70
+ try {
71
+ await WorkspaceService.deleteWorkspace({ workspace: child.id });
72
+ }
73
+ catch (err) {
74
+ sendUserToast(`Failed to delete forked child ${child.id}: ${err}`, true);
75
+ return;
76
+ }
77
+ }
78
+ }
67
79
  await WorkspaceService.deleteWorkspace({ workspace });
68
80
  sendUserToast('You deleted the workspace');
69
81
  clearStores();
70
82
  goto('/user/workspaces');
71
83
  }
84
+ let deleteForkedChildren = $state(false);
85
+ const forkedDescendants = $derived($workspaceStore ? findWorkspaceDescendants($workspaceStore, $userWorkspaces ?? []) : []);
72
86
  let hasNewChangelogs = $state(false);
73
87
  let recentChangelogs = $state([]);
74
88
  let lastOpened = localStorage.getItem('changelogsLastOpened');
@@ -206,7 +220,6 @@ let mainMenuLinks = $derived([
206
220
  label: 'Assets',
207
221
  href: `${base}/assets`,
208
222
  icon: Pyramid,
209
- disabled: $userStore?.operator,
210
223
  aiId: 'sidebar-menu-link-assets',
211
224
  aiDescription: 'Button to navigate to assets'
212
225
  },
@@ -403,6 +416,7 @@ let secondaryMenuLinks = $derived([
403
416
  label: 'Delete Forked Workspace',
404
417
  action: async () => {
405
418
  await loadForkedDatatables();
419
+ deleteForkedChildren = false;
406
420
  deleteWorkspaceForkModal = true;
407
421
  },
408
422
  icon: Trash2,
@@ -718,6 +732,30 @@ let secondaryMenuLinks = $derived([
718
732
  >
719
733
  <div class="flex flex-col w-full space-y-4">
720
734
  <span>Are you sure you want to delete this workspace fork? (deleting {$workspaceStore})</span>
735
+ {#if forkedDescendants.length > 0}
736
+ <div class="border rounded-md divide-y">
737
+ <div class="px-4 py-2 flex items-center justify-between gap-2">
738
+ <div class="flex flex-col min-w-0">
739
+ <span class="text-xs font-semibold text-secondary">Forked children</span>
740
+ <span class="text-3xs text-hint">
741
+ This fork has {forkedDescendants.length} forked
742
+ {forkedDescendants.length === 1 ? 'child' : 'children'} (transitively).
743
+ </span>
744
+ </div>
745
+ <Toggle
746
+ class="shrink-0"
747
+ size="xs"
748
+ bind:checked={deleteForkedChildren}
749
+ options={{ right: 'Also delete children' }}
750
+ />
751
+ </div>
752
+ <ul class="px-4 py-2 text-3xs text-hint max-h-32 overflow-y-auto">
753
+ {#each forkedDescendants as child}
754
+ <li class="font-mono truncate" title={child.id}>{child.id}</li>
755
+ {/each}
756
+ </ul>
757
+ </div>
758
+ {/if}
721
759
  {#if forkedDatatables.length > 0}
722
760
  <div class="border rounded-md divide-y">
723
761
  <div class="px-4 py-2 text-xs font-semibold text-secondary"> Forked databases </div>