diff --git a/frontend/src/lib/components/VariableExpirationHandler.svelte b/frontend/src/lib/components/VariableExpirationHandler.svelte new file mode 100644 index 0000000000..422ce6873f --- /dev/null +++ b/frontend/src/lib/components/VariableExpirationHandler.svelte @@ -0,0 +1,429 @@ + + + + +
+ + {#snippet children({ item })} + + + + {/snippet} + + +
+ {#if handlerSelected === 'custom'} +
+ + {#if !handlerPath} + + {/if} +
+ {#if handlerPath} +
+

Extra arguments

+ {#await import('$lib/components/SchemaForm.svelte')} + + {:then Module} + + {/await} + {#if customHandlerSchema?.properties && Object.keys(customHandlerSchema.properties).length === 0} +
This handler takes no extra arguments
+ {/if} +
+ {/if} + {:else if handlerSelected === 'slack'} + + + {#if workspaceConnectedToSlack} + { + handlerPath = e.detail ? hubPaths.slackVariableExpirationHandler : undefined + }} + /> + {/if} + + {#if workspaceConnectedToSlack && isSlackHandler} +
+ {#await import('$lib/components/SchemaForm.svelte')} + + {:then Module} + + {/await} +
+ {:else if workspaceConnectedToSlack === undefined} + + {/if} + {:else if handlerSelected === 'teams'} + + + {#if workspaceConnectedToTeams} + { + handlerPath = e.detail ? hubPaths.teamsVariableExpirationHandler : undefined + }} + /> + +
+
+

Teams channel

+ +
+ { + handlerExtraArgs[CHANNEL_KEY] = channel?.channel_id + handlerExtraArgs[CHANNEL_NAME_KEY] = channel?.channel_name + }} + onError={(e) => sendUserToast('Failed to load channels: ' + e.message, true)} + /> +
+ {:else if workspaceConnectedToTeams === undefined} + + {/if} + {/if} + + {#if (isSlackHandler || isTeamsHandler) && isEditable} +
+ +

+ Runs the handler now against a placeholder variable, as g/variable_expiration_handler. + Save the handler first: the test runs what is stored. +

+ {#if testJob !== undefined} +
+

+ {#if testJob.in_progress} + + Sending message... + {:else if testJob.is_success} + + Message sent via Windmill job + {:else} + + Message not sent + {/if} + + {testJob.uuid} + + +

+
+ {/if} +
+ {/if} + + {#if handlerSelected !== 'custom' && workspaceConnectedToSlack === false && handlerSelected === 'slack'} + + Connect this workspace to Slack to notify a channel without writing a script. + + {/if} +
+
diff --git a/frontend/src/lib/hub.ts b/frontend/src/lib/hub.ts index 25b3f8b3fd..dfbc6b3723 100644 --- a/frontend/src/lib/hub.ts +++ b/frontend/src/lib/hub.ts @@ -59,6 +59,13 @@ type HubPaths = { teamsErrorHandler: string teamsRecoveryHandler: string teamsSuccessHandler: string + // TODO(WIN-2332): the ids of these two are placeholders until the scripts are pushed to + // the hub, which assigns them. Nothing resolves them meanwhile, and the image build runs + // `windmill cache hubPaths.json` over every entry (Dockerfile), so it fails on an id the + // hub does not have. Replace both before merging; only the number matters, the slug is + // ours and is what the settings tab matches on. + slackVariableExpirationHandler: string + teamsVariableExpirationHandler: string emailErrorHandler: string cloneRepoToS3forGitRepoViewer: string appReport: string diff --git a/frontend/src/lib/hubPaths.json b/frontend/src/lib/hubPaths.json index 2862fc7768..feb01e9246 100644 --- a/frontend/src/lib/hubPaths.json +++ b/frontend/src/lib/hubPaths.json @@ -8,6 +8,8 @@ "teamsErrorHandler": "hub/19742/workspace-or-schedule-error-handler-teams", "teamsRecoveryHandler": "hub/11593/schedule-recovery-handler-teams", "teamsSuccessHandler": "hub/11596/schedule-success-handler-teams", + "slackVariableExpirationHandler": "hub/99999901/variable-expiration-handler-slack", + "teamsVariableExpirationHandler": "hub/99999902/variable-expiration-handler-teams", "slackReport": "hub/28792/slack", "discordReport": "hub/9085/discord", "smtpReport": "hub/28242/smtp", diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 27696443ce..966767a3fa 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -10,6 +10,10 @@ import DeployToSetting from '$lib/components/DeployToSetting.svelte' import DevWorkspaceSetting from '$lib/components/DevWorkspaceSetting.svelte' import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte' + import VariableExpirationHandler, { + getVariableExpirationHandlerType, + type VariableExpirationHandlerType + } from '$lib/components/VariableExpirationHandler.svelte' import PageHeader from '$lib/components/PageHeader.svelte' import ScriptPicker from '$lib/components/ScriptPicker.svelte' @@ -188,10 +192,12 @@ let variableExpirationPath: string | undefined = $state(undefined) let variableExpirationItemKind: 'flow' | 'script' = $state('script') let variableExpirationMutedOnUserPath: boolean = $state(false) - // Carried through saves rather than edited: `extra_args` is an API/CLI-only escape hatch - // for passing fixed arguments to the handler, and omitting it on a save that keeps the - // handler would erase it. Only a save that clears the handler drops it. - let variableExpirationExtraArgs: Record | undefined = $state(undefined) + let variableExpirationSelected: VariableExpirationHandlerType = $state('custom') + // The built-in destinations write `channel` here, and the API and CLI can set arbitrary + // fixed arguments the tab never renders. Both have to survive a save that keeps the + // handler, since the endpoint rebuilds the setting from the request and an omitted + // `extra_args` erases it. Only a save that clears the handler drops it. + let variableExpirationExtraArgs: Record = $state({}) let criticalAlertUIMuted: boolean | undefined = $state(undefined) let initialCriticalAlertUIMuted: boolean | undefined = $state(undefined) let publicAppRateLimitPerMinute: number | undefined = $state(undefined) @@ -249,6 +255,7 @@ let initialVariableExpirationPath: string | undefined = $state(undefined) let initialVariableExpirationItemKind: 'flow' | 'script' = $state('script') let initialVariableExpirationMutedOnUserPath: boolean = $state(false) + let initialVariableExpirationExtraArgs: Record = $state({}) let s3ResourceSettings: S3ResourceSettings = $state({ resourceType: 's3', @@ -324,11 +331,13 @@ initialVariableExpirationItemKind, initialVariableExpirationPath ), - mutedOnUserPath: initialVariableExpirationMutedOnUserPath + mutedOnUserPath: initialVariableExpirationMutedOnUserPath, + extraArgs: normalizeHandlerExtraArgs(initialVariableExpirationExtraArgs) }, { handlerPath: variableExpirationFullPath(variableExpirationItemKind, variableExpirationPath), - mutedOnUserPath: variableExpirationMutedOnUserPath + mutedOnUserPath: variableExpirationMutedOnUserPath, + extraArgs: normalizeHandlerExtraArgs(variableExpirationExtraArgs) } ) }) @@ -693,7 +702,8 @@ : 'script' variableExpirationPath = variableExpirationHandlerPath.split('/').slice(1).join('/') variableExpirationMutedOnUserPath = variableExpirationHandler?.muted_on_user_path ?? false - variableExpirationExtraArgs = variableExpirationHandler?.extra_args + variableExpirationExtraArgs = variableExpirationHandler?.extra_args ?? {} + variableExpirationSelected = getVariableExpirationHandlerType(variableExpirationPath) workspaceDefaultAppPath = settings.default_app initialWorkspaceDefaultAppPath = settings.default_app @@ -750,6 +760,7 @@ initialVariableExpirationPath = variableExpirationPath initialVariableExpirationItemKind = variableExpirationItemKind initialVariableExpirationMutedOnUserPath = variableExpirationMutedOnUserPath + initialVariableExpirationExtraArgs = clone(variableExpirationExtraArgs) loadedSettings = true } @@ -925,11 +936,11 @@ async function editVariableExpirationHandler() { const path = variableExpirationFullPath(variableExpirationItemKind, variableExpirationPath) // Saving with no path clears the whole setting server-side, `extra_args` included, so - // the loaded copy has to go too: a handler picked afterwards without reloading the page - // would otherwise be persisted with the cleared one's arguments, which this tab does - // not display. + // the in-memory copy has to go too: a handler picked afterwards without reloading the + // page would otherwise be persisted with the cleared one's arguments, including any the + // API or CLI set that this tab never renders. if (!path) { - variableExpirationExtraArgs = undefined + variableExpirationExtraArgs = {} } await WorkspaceService.editVariableExpirationHandler({ workspace: $workspaceStore!, @@ -950,12 +961,15 @@ initialVariableExpirationPath = variableExpirationPath initialVariableExpirationItemKind = variableExpirationItemKind initialVariableExpirationMutedOnUserPath = variableExpirationMutedOnUserPath + initialVariableExpirationExtraArgs = clone(variableExpirationExtraArgs) } function discardVariableExpirationChanges() { variableExpirationPath = initialVariableExpirationPath variableExpirationItemKind = initialVariableExpirationItemKind variableExpirationMutedOnUserPath = initialVariableExpirationMutedOnUserPath + variableExpirationExtraArgs = clone(initialVariableExpirationExtraArgs) + variableExpirationSelected = getVariableExpirationHandlerType(variableExpirationPath) } async function editSuccessHandler() { @@ -1219,14 +1233,16 @@ initialVariableExpirationItemKind, initialVariableExpirationPath ), - mutedOnUserPath: initialVariableExpirationMutedOnUserPath + mutedOnUserPath: initialVariableExpirationMutedOnUserPath, + extraArgs: normalizeHandlerExtraArgs(initialVariableExpirationExtraArgs) }, modifiedValue: { handlerPath: variableExpirationFullPath( variableExpirationItemKind, variableExpirationPath ), - mutedOnUserPath: variableExpirationMutedOnUserPath + mutedOnUserPath: variableExpirationMutedOnUserPath, + extraArgs: normalizeHandlerExtraArgs(variableExpirationExtraArgs) } } case 'critical_alerts': @@ -2120,26 +2136,22 @@ export async function main(
-
- - - -
+ })} + />