From 80bcf05fa623a26ed48c5af7eede21796e58e611 Mon Sep 17 00:00:00 2001 From: Alex Petric Date: Thu, 6 Feb 2025 10:54:14 -0500 Subject: [PATCH] workspace error handler --- backend/windmill-api/src/workspaces.rs | 18 +- frontend/openapi-ts-error-1738700665225.log | 28 +++ .../components/ErrorOrRecoveryHandler.svelte | 218 ++++++++++++++++-- .../lib/components/ScheduleEditorInner.svelte | 6 +- frontend/src/lib/hub.ts | 3 + frontend/src/lib/hubPaths.json | 3 + .../(logged)/workspace_settings/+page.svelte | 17 +- 7 files changed, 257 insertions(+), 36 deletions(-) create mode 100644 frontend/openapi-ts-error-1738700665225.log diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index faaacb0ddb..244f95215c 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -58,7 +58,10 @@ use sqlx::{FromRow, Postgres, Transaction}; use windmill_common::oauth2::InstanceEvent; use windmill_common::utils::not_found_if_none; -use crate::teams_ee::{edit_teams_command, workspaces_list_available_teams_ids, connect_teams}; +use crate::teams_ee::{ + connect_teams, edit_teams_command, run_teams_message_test_job, + workspaces_list_available_teams_channels, workspaces_list_available_teams_ids, +}; lazy_static::lazy_static! { static ref WORKSPACE_KEY_REGEXP: Regex = Regex::new("^[a-zA-Z0-9]{64}$").unwrap(); @@ -76,12 +79,23 @@ pub fn workspaced_service() -> Router { .route("/get_deploy_to", get(get_deploy_to)) .route("/edit_slack_command", post(edit_slack_command)) .route("/edit_teams_command", post(edit_teams_command)) - .route("/available_teams_ids", get(workspaces_list_available_teams_ids)) + .route( + "/available_teams_ids", + get(workspaces_list_available_teams_ids), + ) + .route( + "/available_teams_channels", + get(workspaces_list_available_teams_channels), + ) .route("/connect_teams", post(connect_teams)) .route( "/run_slack_message_test_job", post(run_slack_message_test_job), ) + .route( + "/run_teams_message_test_job", + post(run_teams_message_test_job), + ) .route("/edit_webhook", post(edit_webhook)) .route("/edit_auto_invite", post(edit_auto_invite)) .route("/edit_deploy_to", post(edit_deploy_to)) diff --git a/frontend/openapi-ts-error-1738700665225.log b/frontend/openapi-ts-error-1738700665225.log new file mode 100644 index 0000000000..3e9f583aee --- /dev/null +++ b/frontend/openapi-ts-error-1738700665225.log @@ -0,0 +1,28 @@ +Error parsing /home/alex/windmill/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (3222:3) + + 3219 | schema: + 3220 | type: string + 3221 | + 3222 | /w/{workspace}/oauth/disconnec ... +----------^ + 3223 | post: + 3224 | summary: disconnect slack +ParserError: Error parsing /home/alex/windmill/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (3222:3) + + 3219 | schema: + 3220 | type: string + 3221 | + 3222 | /w/{workspace}/oauth/disconnec ... +----------^ + 3223 | post: + 3224 | summary: disconnect slack + at Object.parse (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parsers/yaml.js:44:23) + at getResult (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:116:22) + at runNextPlugin (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:64:32) + at /home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:55:9 + at new Promise () + at Object.run (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:54:12) + at parseFile (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:130:38) + at parse (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:56:30) + at async $RefParser.parse (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:115:28) + at async $RefParser.resolve (/home/alex/windmill/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:145:13) \ No newline at end of file diff --git a/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte b/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte index 57674aedef..4ca67f3029 100644 --- a/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte +++ b/frontend/src/lib/components/ErrorOrRecoveryHandler.svelte @@ -5,6 +5,7 @@ import type { Schema, SupportedLanguage } from '$lib/common' import { base } from '$lib/base' import { enterpriseLicense, workspaceStore } from '$lib/stores' + import MsTeamsIcon from '$lib/components/icons/MSTeamsIcon.svelte' import { emptySchema, emptyString, sendUserToast, tryEvery } from '$lib/utils' import { FlowService, @@ -14,21 +15,26 @@ WorkspaceService, type Flow } from '$lib/gen' + import type { ListAvailableTeamsChannelsResponse } from '$lib/gen/types.gen' import { inferArgs } from '$lib/infer' import { hubBaseUrlStore } from '$lib/stores' - import { CheckCircle2, Loader2, RotateCw, XCircle } from 'lucide-svelte' + import { CheckCircle2, Loader2, RotateCw, XCircle, RefreshCcw } from 'lucide-svelte' import { hubPaths } from '$lib/hub' const slackRecoveryHandler = hubPaths.slackRecoveryHandler const slackHandlerScriptPath = hubPaths.slackErrorHandler const slackSuccessHandler = hubPaths.slackSuccessHandler + const teamsRecoveryHandler = hubPaths.teamsRecoveryHandler + const teamsHandlerScriptPath = hubPaths.teamsErrorHandler + const teamsSuccessHandler = hubPaths.teamsSuccessHandler + export let errorOrRecovery: 'error' | 'recovery' | 'success' export let isEditable: boolean - export let slackToggleText: string = 'Enable' + export let toggleText: string = 'Enable' export let showScriptHelpText: boolean = false - export let handlerSelected: 'custom' | 'slack' + export let handlerSelected: 'custom' | 'slack' | 'teams' export let handlerPath: string | undefined export let handlerExtraArgs: Record @@ -36,13 +42,18 @@ export let customInitialScriptPath: string | undefined export let customScriptTemplate: string export let customHandlerKind: 'flow' | 'script' = 'script' - let customHandlerSchema: Schema | undefined + let customHandlerSchema: Schema | undefined let slackHandlerSchema: Schema | undefined + let teamsHandlerSchema: Schema | undefined + let isFetching: boolean = false + + let teams_channels: ListAvailableTeamsChannelsResponse = [] + let workspaceConnectedToSlack: boolean | undefined = undefined - let slackConnectionTestJob: - | { uuid: string; is_success: boolean; in_progress: boolean } - | undefined + let workspaceConnectedToTeams: boolean | undefined = undefined + + let connectionTestJob: { uuid: string; is_success: boolean; in_progress: boolean } | undefined async function loadSlackResources() { const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! }) @@ -53,16 +64,35 @@ } } - async function sendSlackMessage(channel: string): Promise { - let submitted_job = await WorkspaceService.runSlackMessageTestJob({ + async function loadTeamsResources() { + isFetching = true + const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! }) + if (!emptyString(settings.teams_team_name) && !emptyString(settings.teams_team_id)) { + workspaceConnectedToTeams = true + } else { + workspaceConnectedToTeams = false + } + + teams_channels = await WorkspaceService.listAvailableTeamsChannels({ workspace: $workspaceStore! }) + isFetching = false + } + + async function sendMessage(channel: string, platform: 'teams' | 'slack'): Promise { + const testJobFunction = + platform === 'slack' + ? WorkspaceService.runSlackMessageTestJob + : WorkspaceService.runTeamsMessageTestJob + + let submitted_job = await testJobFunction({ workspace: $workspaceStore!, requestBody: { hub_script_path: handlerPath, channel: channel, - test_msg: `This is a notification to test the connection between Slack and Windmill workspace '${$workspaceStore!}'` + test_msg: `This is a notification to test the connection between ${platform} and Windmill workspace '${$workspaceStore!}'` } }) - slackConnectionTestJob = { + + connectionTestJob = { uuid: submitted_job.job_uuid!, in_progress: true, is_success: false @@ -71,16 +101,16 @@ tryCode: async () => { const testResult = await JobService.getCompletedJob({ workspace: $workspaceStore!, - id: slackConnectionTestJob!.uuid + id: connectionTestJob!.uuid }) - slackConnectionTestJob!.in_progress = false - slackConnectionTestJob!.is_success = testResult.success + connectionTestJob!.in_progress = false + connectionTestJob!.is_success = testResult.success }, timeoutCode: async () => { try { await JobService.cancelQueuedJob({ workspace: $workspaceStore!, - id: slackConnectionTestJob!.uuid, + id: connectionTestJob!.uuid, requestBody: { reason: 'Slack message not sent after 5s' } @@ -94,6 +124,14 @@ }) } + async function sendSlackMessage(channel: string): Promise { + await sendMessage(channel, 'slack') + } + + async function sendTeamsMessage(channel: string): Promise { + await sendMessage(channel, 'teams') + } + async function loadHandlerScriptArgs(p: string, defaultArgs: string[] = []) { try { let schema: Schema | undefined = emptySchema() @@ -145,14 +183,34 @@ } } + function isTeamsHandler(scriptPath: string | undefined) { + if (scriptPath === undefined) { + return false + } + if (errorOrRecovery == 'error') { + return ( + scriptPath.startsWith('hub/') && + scriptPath.endsWith('/workspace-or-schedule-error-handler-teams') + ) + } else if (errorOrRecovery == 'recovery') { + return ( + scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-recovery-handler-teams') + ) + } else { + return scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-success-handler-teams') + } + } + $: { if ($workspaceStore) { loadSlackResources() + loadTeamsResources() } } $: handlerPath && !isSlackHandler(handlerPath) && + !isTeamsHandler(handlerPath) && loadHandlerScriptArgs(handlerPath, [ 'path', 'workspace_id', @@ -187,11 +245,33 @@ 'email', 'slack' ]).then((schema) => (slackHandlerSchema = schema)) + + $: handlerPath && + isTeamsHandler(handlerPath) && + loadHandlerScriptArgs(handlerPath, [ + 'path', + 'workspace_id', + 'job_id', + 'is_flow', + 'schedule_path', + 'error', + 'error_started_at', + 'failed_times', + 'started_at', + 'success_times', + 'success_result', + 'success_started_at', + 'email', + 'teams' + ]).then((schema) => (teamsHandlerSchema = schema))
- + { + //TODO + }} class="mt-2 mb-4"> Slack + Teams Custom @@ -254,7 +334,7 @@ { if (e.detail && errorOrRecovery === 'error') { handlerPath = slackHandlerScriptPath @@ -313,11 +393,11 @@ on:click={() => sendSlackMessage(handlerExtraArgs['channel'])} size="xs">Send test message - {#if slackConnectionTestJob !== undefined} + {#if connectionTestJob !== undefined}

- {#if slackConnectionTestJob.in_progress} + {#if connectionTestJob.in_progress} - {:else if slackConnectionTestJob.is_success} + {:else if connectionTestJob.is_success} {:else} @@ -325,9 +405,103 @@ Message sent via Windmill job - {slackConnectionTestJob.uuid} + {connectionTestJob.uuid} + +

+ {/if} + {/if} + {/if} +{:else if handlerSelected === 'teams'} + + { + if (e.detail && errorOrRecovery === 'error') { + handlerPath = teamsHandlerScriptPath + } else if (e.detail && errorOrRecovery === 'recovery') { + handlerPath = teamsRecoveryHandler + } else if (e.detail && errorOrRecovery === 'success') { + handlerPath = teamsSuccessHandler + } else { + handlerPath = undefined + } + }} + /> + + {#if workspaceConnectedToTeams} +
+
+ +
+

Teams Channel

+
+ +
+
+ +
+
+ {:else if workspaceConnectedToTeams == undefined} + + {/if} + {#if $enterpriseLicense && isTeamsHandler(handlerPath)} + {#if workspaceConnectedToTeams == false} + +
+

+ The workspace needs to be connected to Teams to use this feature. You can configure it here. +

+
+
+ {:else} + + {#if connectionTestJob !== undefined} +

+ {#if connectionTestJob.in_progress} + + {:else if connectionTestJob.is_success} + + {:else} + + {/if} + Message sent via Windmill job + + {connectionTestJob.uuid}

{/if} diff --git a/frontend/src/lib/components/ScheduleEditorInner.svelte b/frontend/src/lib/components/ScheduleEditorInner.svelte index 261bbe9327..2afa339bc0 100644 --- a/frontend/src/lib/components/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/ScheduleEditorInner.svelte @@ -842,7 +842,7 @@ bind:handlerSelected={errorHandlerSelected} bind:handlerPath={errorHandlerPath} customInitialScriptPath={errorHandlerCustomInitialPath} - slackToggleText="Alert channel on error" + toggleText="Alert channel on error" customScriptTemplate="/scripts/add?hub=hub%2F9081%2Fwindmill%2Fschedule_error_handler_template" bind:customHandlerKind={errorHandleritemKind} bind:handlerExtraArgs={errorHandlerExtraArgs} @@ -937,7 +937,7 @@ bind:handlerSelected={recoveryHandlerSelected} bind:handlerPath={recoveryHandlerPath} customInitialScriptPath={recoveryHandlerCustomInitialPath} - slackToggleText="Alert channel when error recovered" + toggleText="Alert channel when error recovered" customScriptTemplate="/scripts/add?hub=hub%2F9082%2Fwindmill%2Fschedule_recovery_handler_template" bind:customHandlerKind={recoveryHandlerItemKind} bind:handlerExtraArgs={recoveryHandlerExtraArgs} @@ -1030,7 +1030,7 @@ bind:handlerSelected={successHandlerSelected} bind:handlerPath={successHandlerPath} customInitialScriptPath={successHandlerCustomInitialPath} - slackToggleText="Alert channel when successful" + toggleText="Alert channel when successful" customScriptTemplate="/scripts/add?hub=hub%2F9071%2Fwindmill%2Fschedule_success_handler_template" bind:customHandlerKind={successHandlerItemKind} bind:handlerExtraArgs={successHandlerExtraArgs} diff --git a/frontend/src/lib/hub.ts b/frontend/src/lib/hub.ts index caa49e5948..c856775612 100644 --- a/frontend/src/lib/hub.ts +++ b/frontend/src/lib/hub.ts @@ -66,6 +66,9 @@ type HubPaths = { slackReport: string discordReport: string smtpReport: string + teamsErrorHandler: string + teamsRecoveryHandler: string + teamsSuccessHandler: string } export const hubPaths = JSON.parse(rawHubPaths) as HubPaths diff --git a/frontend/src/lib/hubPaths.json b/frontend/src/lib/hubPaths.json index a9c887fe19..52eac9278f 100644 --- a/frontend/src/lib/hubPaths.json +++ b/frontend/src/lib/hubPaths.json @@ -10,6 +10,9 @@ "slackErrorHandler_0": "hub/9079/workspace-or-schedule-error-handler-slack", "slackRecoveryHandler": "hub/9080/slack/schedule-recovery-handler-slack", "slackSuccessHandler": "hub/9072/slack/schedule-success-handler-slack", + "teamsErrorHandler": "hub/9081/workspace-or-schedule-error-handler-teams", + "teamsRecoveryHandler": "hub/9082/teams/schedule-recovery-handler-teams", + "teamsSuccessHandler": "hub/9073/teams/schedule-success-handler-teams", "slackReport": "hub/9084/slack", "discordReport": "hub/9085/discord", "smtpReport": "hub/9086/smtp" diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index f5c2d6529f..5c3814b922 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -106,7 +106,7 @@ let customer_id: string | undefined = undefined let webhook: string | undefined = undefined let workspaceToDeployTo: string | undefined = undefined - let errorHandlerSelected: 'custom' | 'slack' = 'slack' + let errorHandlerSelected: 'custom' | 'slack' | 'teams' = 'slack' let errorHandlerInitialScriptPath: string let errorHandlerScriptPath: string let errorHandlerItemKind: 'flow' | 'script' = 'script' @@ -437,12 +437,11 @@ if (emptyString($enterpriseLicense)) { errorHandlerSelected = 'custom' } else { - errorHandlerSelected = - emptyString(errorHandlerScriptPath) || - (errorHandlerScriptPath.startsWith('hub/') && - errorHandlerScriptPath.endsWith('/workspace-or-schedule-error-handler-slack')) - ? 'slack' - : 'custom' + errorHandlerSelected = + emptyString(errorHandlerScriptPath) ? 'custom' : + (errorHandlerScriptPath.startsWith('hub/') && errorHandlerScriptPath.endsWith('/workspace-or-schedule-error-handler-slack')) ? 'slack' : + (errorHandlerScriptPath.endsWith('/workspace-or-schedule-error-handler-teams')) ? 'teams' : + 'custom' } errorHandlerExtraArgs = settings.error_handler_extra_args ?? {} codeCompletionEnabled = settings.code_completion_enabled @@ -991,7 +990,7 @@