workspace error handler

This commit is contained in:
Alex Petric
2025-02-06 10:54:14 -05:00
parent db17d75e60
commit 80bcf05fa6
7 changed files with 257 additions and 36 deletions
+16 -2
View File
@@ -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))
@@ -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 (<anonymous>)
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)
@@ -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<string, any>
@@ -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<void> {
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<void> {
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<void> {
await sendMessage(channel, 'slack')
}
async function sendTeamsMessage(channel: string): Promise<void> {
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))
</script>
<div>
<Tabs bind:selected={handlerSelected} class="mt-2 mb-4">
<Tabs bind:selected={handlerSelected} on:change={() => {
//TODO
}} class="mt-2 mb-4">
<Tab value="slack" disabled={!isEditable}>Slack</Tab>
<Tab value="teams" disabled={!isEditable}>Teams</Tab>
<Tab value="custom" disabled={!isEditable}>
Custom
<slot name="custom-tab-tooltip" />
@@ -254,7 +334,7 @@
<Toggle
disabled={!$enterpriseLicense || !isEditable}
checked={isSlackHandler(handlerPath)}
options={{ right: slackToggleText }}
options={{ right: toggleText }}
on:change={async (e) => {
if (e.detail && errorOrRecovery === 'error') {
handlerPath = slackHandlerScriptPath
@@ -313,11 +393,11 @@
on:click={() => sendSlackMessage(handlerExtraArgs['channel'])}
size="xs">Send test message</Button
>
{#if slackConnectionTestJob !== undefined}
{#if connectionTestJob !== undefined}
<p class="text-normal text-2xs mt-1 gap-2">
{#if slackConnectionTestJob.in_progress}
{#if connectionTestJob.in_progress}
<RotateCw size={14} />
{:else if slackConnectionTestJob.is_success}
{:else if connectionTestJob.is_success}
<CheckCircle2 size={14} class="text-green-600" />
{:else}
<XCircle size={14} class="text-red-700" />
@@ -325,9 +405,103 @@
Message sent via Windmill job
<a
target="_blank"
href={`${base}/run/${slackConnectionTestJob.uuid}?workspace=${$workspaceStore}`}
href={`${base}/run/${connectionTestJob.uuid}?workspace=${$workspaceStore}`}
>
{slackConnectionTestJob.uuid}
{connectionTestJob.uuid}
</a>
</p>
{/if}
{/if}
{/if}
{:else if handlerSelected === 'teams'}
<span class="w-full flex mb-3">
<Toggle
disabled={!$enterpriseLicense || !isEditable}
checked={isTeamsHandler(handlerPath)}
options={{ right: toggleText }}
on:change={async (e) => {
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
}
}}
/>
</span>
{#if workspaceConnectedToTeams}
<div class="w-1/2 flex flex-row items-center gap-2">
<div class="pt-1 flex-shrink-0">
<MsTeamsIcon height="24px" width="24px" />
</div>
<p class="text-sm">Teams Channel</p>
<div class="flex-grow">
<select class="w-full" bind:value={handlerExtraArgs['teams_channel']}>
{#if teams_channels.length === 0}
<option value="" disabled selected>Bot not connected to any channel</option>
{:else}
<option value="" disabled selected>Select Teams channel</option>
{#each teams_channels as channel}
<option value={channel.channel_id}>
{channel.channel_name}
</option>
{/each}
{/if}
</select>
</div>
<div class="flex-shrink-0">
<button on:click={loadTeamsResources} class="flex items-center gap-1 mt-2">
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
</button>
</div>
</div>
{:else if workspaceConnectedToTeams == undefined}
<Loader2 class="animate-spin" size={10} />
{/if}
{#if $enterpriseLicense && isTeamsHandler(handlerPath)}
{#if workspaceConnectedToTeams == false}
<Alert type="error" title="Workspace not connected to Teams">
<div class="flex flex-row gap-x-1 w-full items-center">
<p class="text-clip grow min-w-0">
The workspace needs to be connected to Teams to use this feature. You can <a
target="_blank"
href="{base}/workspace_settings?tab=slack">configure it here</a
>.
</p>
<Button
variant="border"
color="light"
on:click={loadTeamsResources}
startIcon={{ icon: RotateCw }}
/>
</div>
</Alert>
{:else}
<Button
disabled={emptyString(handlerExtraArgs['teams_channel'])}
btnClasses="w-32 text-center"
color="dark"
on:click={() => sendTeamsMessage(handlerExtraArgs['teams_channel'])}
size="xs">Send test message</Button
>
{#if connectionTestJob !== undefined}
<p class="text-normal text-2xs mt-1 gap-2">
{#if connectionTestJob.in_progress}
<RotateCw size={14} />
{:else if connectionTestJob.is_success}
<CheckCircle2 size={14} class="text-green-600" />
{:else}
<XCircle size={14} class="text-red-700" />
{/if}
Message sent via Windmill job
<a
target="_blank"
href={`${base}/run/${connectionTestJob.uuid}?workspace=${$workspaceStore}`}
>
{connectionTestJob.uuid}
</a>
</p>
{/if}
@@ -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}
+3
View File
@@ -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
+3
View File
@@ -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"
@@ -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 @@
<div class="flex flex-col mt-5 gap-5 items-start">
<Toggle
disabled={!$enterpriseLicense ||
(errorHandlerSelected === 'slack' &&
((errorHandlerSelected === 'slack' || errorHandlerSelected === 'teams') &&
!emptyString(errorHandlerScriptPath) &&
emptyString(errorHandlerExtraArgs['channel']))}
bind:checked={errorHandlerMutedOnCancel}
@@ -999,7 +998,7 @@
/>
<Button
disabled={!$enterpriseLicense ||
(errorHandlerSelected === 'slack' &&
((errorHandlerSelected === 'slack' || errorHandlerSelected === 'teams') &&
!emptyString(errorHandlerScriptPath) &&
emptyString(errorHandlerExtraArgs['channel']))}
size="sm"