feat: Workspace error handler now supports args and Slack for EE (#2447)

* feat: Workspace error handler now supports args and Slack

* Cleanup unused value

* run sqlx prepare

* Fix script path loading
This commit is contained in:
Guillaume Bouvignies
2023-10-13 08:32:11 -07:00
committed by GitHub
parent 221965546d
commit a55cb50b5d
24 changed files with 397 additions and 277 deletions
@@ -37,7 +37,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -72,6 +72,11 @@
"ordinal": 13,
"name": "code_completion_enabled",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "error_handler_extra_args",
"type_info": "Json"
}
],
"parameters": {
@@ -93,7 +98,8 @@
true,
true,
true,
false
false,
true
]
},
"hash": "1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597"
@@ -67,7 +67,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -28,7 +28,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -72,6 +72,11 @@
"ordinal": 13,
"name": "code_completion_enabled",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "error_handler_extra_args",
"type_info": "Json"
}
],
"parameters": {
@@ -93,7 +98,8 @@
true,
true,
true,
false
false,
true
]
},
"hash": "5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e"
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -46,7 +46,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -40,7 +40,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -60,7 +60,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "c0635f65d561c1a6b183b8fea45320482375d0bb5e617a9dd236a631a7b9ff05"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = NULL, error_handler_extra_args = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "d9308c0154e029f7568b1d021368ca07f4867d192aa58b3888ae532d1c040828"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = $1 WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "e844b9ee75a1b5438ffe743185fc78f649db89c78b2b8cd7e778bf18a6b41e67"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = $1, error_handler_extra_args = $2 WHERE workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Json",
"Text"
]
},
"nullable": []
},
"hash": "ea2ad5213bb6de8eab502fbb76149fa7964cd11e74e5ed1a30e857aad8ea5595"
}
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE workspace_settings DROP COLUMN error_handler_extra_args;
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN error_handler_extra_args JSON;
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1158,6 +1158,8 @@ paths:
type: boolean
error_handler:
type: string
error_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
required:
- code_completion_enabled
@@ -1419,6 +1421,8 @@ paths:
properties:
error_handler:
type: string
error_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
+2 -1
View File
@@ -95,7 +95,6 @@ async fn create_schedule(
Json(ns): Json<NewSchedule>,
) -> Result<String> {
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
#[cfg(not(feature = "enterprise"))]
if ns.on_recovery.is_some() {
@@ -121,6 +120,8 @@ async fn create_schedule(
));
}
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
check_path_conflict(tx.transaction_mut(), &w_id, &ns.path).await?;
check_flow_conflict(
+14 -3
View File
@@ -132,6 +132,7 @@ pub struct WorkspaceSettings {
pub openai_resource_path: Option<String>,
pub code_completion_enabled: bool,
pub error_handler: Option<String>,
pub error_handler_extra_args: Option<serde_json::Value>,
}
#[derive(FromRow, Serialize, Debug)]
@@ -243,6 +244,7 @@ pub struct NewWorkspaceUser {
#[derive(Deserialize)]
pub struct EditErrorHandler {
pub error_handler: Option<String>,
pub error_handler_extra_args: Option<serde_json::Value>,
}
async fn list_pending_invites(
@@ -807,6 +809,15 @@ async fn edit_error_handler(
) -> Result<String> {
require_admin(is_admin, &username)?;
#[cfg(not(feature = "enterprise"))]
if ee.error_handler.is_some()
&& ee.error_handler.as_ref().unwrap() == "script/hub/2431/slack/schedule-error-handler-slack"
{
return Err(Error::BadRequest(
"Slack error handler is only available in enterprise version".to_string(),
));
}
let mut tx = db.begin().await?;
sqlx::query_as!(
@@ -821,17 +832,17 @@ async fn edit_error_handler(
.await?;
if let Some(error_handler) = &ee.error_handler {
sqlx::query!(
"UPDATE workspace_settings SET error_handler = $1 WHERE workspace_id = $2",
"UPDATE workspace_settings SET error_handler = $1, error_handler_extra_args = $2 WHERE workspace_id = $3",
error_handler,
ee.error_handler_extra_args,
&w_id
)
.execute(&mut *tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET error_handler = NULL WHERE workspace_id = $1",
"UPDATE workspace_settings SET error_handler = NULL, error_handler_extra_args = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut *tx)
@@ -17,11 +17,11 @@
export let showScriptHelpText: boolean = false
export let handlerSelected: 'custom' | 'slack'
export let handlersOnlyForEe: string[]
export let customScriptTooltip: string | undefined = undefined
export let handlerPath: string | undefined
export let handlerExtraArgs: Record<string, any>
export let customInitialScriptPath: string | undefined
export let customScriptTemplate: string
export let customHandlerKind: 'flow' | 'script' = 'script'
let customHandlerSchema: Schema | undefined
@@ -168,7 +168,7 @@
<div class="flex flex-row mb-2">
<ScriptPicker
disabled={!isEditable}
initialPath={handlerPath}
initialPath={customInitialScriptPath}
kind={Script.kind.SCRIPT}
allowFlow={true}
bind:scriptPath={handlerPath}
@@ -28,9 +28,11 @@
let itemKind: 'flow' | 'script' = 'script'
let errorHandleritemKind: 'flow' | 'script' = 'script'
let errorHandlerPath: string | undefined = undefined
let errorHandlerCustomInitialPath: string | undefined = undefined
let errorHandlerSelected: 'custom' | 'slack' = 'slack'
let errorHandlerExtraArgs: Record<string, any> = {}
let recoveryHandlerPath: string | undefined = undefined
let recoveryHandlerCustomInitialPath: string | undefined = undefined
let recoveryHandlerSelected: 'custom' | 'slack' = 'slack'
let recoveryHandlerItemKind: 'flow' | 'script' = 'script'
let recoveryHandlerExtraArgs: Record<string, any> = {}
@@ -64,9 +66,11 @@
errorHandlerSelected = $enterpriseLicense ? 'slack' : 'custom'
errorHandleritemKind = 'script'
errorHandlerPath = undefined
errorHandlerCustomInitialPath = undefined
errorHandlerExtraArgs = {}
recoveryHandlerSelected = $enterpriseLicense ? 'slack' : 'custom'
recoveryHandlerPath = undefined
recoveryHandlerCustomInitialPath = undefined
recoveryHandlerItemKind = 'script'
recoveryHandlerExtraArgs = {}
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
@@ -125,6 +129,7 @@
let splitted = s.on_failure.split('/')
errorHandleritemKind = splitted[0] as 'flow' | 'script'
errorHandlerPath = splitted.slice(1)?.join('/')
errorHandlerCustomInitialPath = errorHandlerPath
failedTimes = s.on_failure_times ?? 1
failedExact = s.on_failure_exact ?? false
errorHandlerExtraArgs = s.on_failure_extra_args ?? {}
@@ -139,6 +144,7 @@
let splitted = s.on_recovery.split('/')
recoveryHandlerItemKind = splitted[0] as 'flow' | 'script'
recoveryHandlerPath = splitted.slice(1)?.join('/')
recoveryHandlerCustomInitialPath = recoveryHandlerPath
recoveredTimes = s.on_recovery_times ?? 1
recoveryHandlerExtraArgs = s.on_recovery_extra_args ?? {}
if (recoveryHandlerPath !== slackRecoveryHandler) {
@@ -173,7 +179,7 @@
on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined,
on_failure_times: failedTimes,
on_failure_exact: failedExact,
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : {},
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined,
on_recovery: recoveryHandlerPath
? `${recoveryHandlerItemKind}/${recoveryHandlerPath}`
: undefined,
@@ -196,7 +202,7 @@
on_failure: errorHandlerPath ? `${errorHandleritemKind}/${errorHandlerPath}` : undefined,
on_failure_times: failedTimes,
on_failure_exact: failedExact,
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : {},
on_failure_extra_args: errorHandlerPath ? errorHandlerExtraArgs : undefined,
on_recovery: recoveryHandlerPath
? `${recoveryHandlerItemKind}/${recoveryHandlerPath}`
: undefined,
@@ -331,9 +337,9 @@
isEditable={can_write}
handlersOnlyForEe={['slack']}
showScriptHelpText={true}
customScriptTooltip="Hello"
bind:handlerSelected={errorHandlerSelected}
bind:handlerPath={errorHandlerPath}
customInitialScriptPath={errorHandlerCustomInitialPath}
slackHandlerScriptPath={slackErrorHandler}
slackToggleText="Alert channel on error"
customScriptTemplate="/scripts/add?hub=hub%2F2420%2Fwindmill%2Fschedule_error_handler_template"
@@ -395,6 +401,7 @@
handlersOnlyForEe={[]}
bind:handlerSelected={recoveryHandlerSelected}
bind:handlerPath={recoveryHandlerPath}
customInitialScriptPath={recoveryHandlerCustomInitialPath}
slackHandlerScriptPath={slackRecoveryHandler}
slackToggleText="Alert channel when error recovered"
customScriptTemplate="/scripts/add?hub=hub%2F2421%2Fwindmill%2Fschedule_recovery_handler_template"
@@ -6,6 +6,7 @@
import { Alert, Badge, Button, Tab, Tabs } from '$lib/components/common'
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
import PageHeader from '$lib/components/PageHeader.svelte'
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
@@ -23,7 +24,7 @@
workspaceStore
} from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { setQueryWithoutLoad } from '$lib/utils'
import { setQueryWithoutLoad, emptyString } from '$lib/utils'
import { faSlack } from '@fortawesome/free-brands-svg-icons'
import { faBarsStaggered, faScroll } from '@fortawesome/free-solid-svg-icons'
import { Slack } from 'lucide-svelte'
@@ -31,6 +32,8 @@
import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte'
import Toggle from '$lib/components/Toggle.svelte'
const slackErrorHandler = 'hub/2431/slack/schedule-error-handler-slack'
let initialPath: string
let scriptPath: string
let team_name: string | undefined
@@ -39,9 +42,11 @@
let customer_id: string | undefined = undefined
let webhook: string | undefined = undefined
let workspaceToDeployTo: string | undefined = undefined
let errorHandlerInitialPath: string
let errorHandlerSelected: 'custom' | 'slack' = 'slack'
let errorHandlerInitialScriptPath: string
let errorHandlerScriptPath: string
let errorHandlerItemKind: 'script' = 'script'
let errorHandlerItemKind: 'flow' | 'script' = 'script'
let errorHandlerExtraArgs: Record<string, any> = {}
let openaiResourceInitialPath: string | undefined = undefined
let codeCompletionEnabled: boolean = false
let tab =
@@ -159,7 +164,13 @@
webhook = settings.webhook
openaiResourceInitialPath = settings.openai_resource_path
errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/')
errorHandlerInitialPath = errorHandlerScriptPath
errorHandlerInitialScriptPath = errorHandlerScriptPath
if (emptyString($enterpriseLicense)) {
errorHandlerSelected = 'custom'
} else {
errorHandlerSelected = emptyString(errorHandlerScriptPath) || errorHandlerScriptPath === slackErrorHandler ? 'slack' : 'custom'
}
errorHandlerExtraArgs = settings.error_handler_extra_args ?? {}
codeCompletionEnabled = settings.code_completion_enabled
}
@@ -170,17 +181,22 @@
}
async function editErrorHandler() {
errorHandlerInitialPath = errorHandlerScriptPath
if (errorHandlerScriptPath) {
await WorkspaceService.editErrorHandler({
workspace: $workspaceStore!,
requestBody: { error_handler: `${errorHandlerItemKind}/${errorHandlerScriptPath}` }
requestBody: {
error_handler: `${errorHandlerItemKind}/${errorHandlerScriptPath}`,
error_handler_extra_args: errorHandlerExtraArgs,
}
})
sendUserToast(`workspace error handler set to ${errorHandlerScriptPath}`)
} else {
await WorkspaceService.editErrorHandler({
workspace: $workspaceStore!,
requestBody: { error_handler: undefined }
requestBody: {
error_handler: undefined,
error_handler_extra_args: undefined,
}
})
sendUserToast(`workspace error handler removed`)
}
@@ -422,41 +438,51 @@
</div>
{:else if tab == 'error_handler'}
<PageHeader title="Script to run as error handler" primary={false} />
<ScriptPicker
kind={undefined}
bind:itemKind={errorHandlerItemKind}
bind:scriptPath={errorHandlerScriptPath}
initialPath={errorHandlerInitialPath}
on:select={editErrorHandler}
allowRefresh
/>
<div class="flex flex-col gap-20 items-start mt-3">
<div class="w-2/3">
<div class="text-tertiary text-xs">
The following args will be passed to the error handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that errored.</li>
<li><b>email</b>: The email of the user who ran the script or flow that errored.</li>
<li><b>error</b>: The error details.</li>
<li><b>job_id</b>: The job id.</li>
<li><b>is_flow</b>: Whether the error comes from a flow.</li>
<li><b>workspace_id</b>: The workspace id of the failed script or flow.</li>
</ul>
<br />
The error handler will be executed by the automatically created group g/error_handler. If
your error handler requires variables or resources, you need to add them to the group.
</div>
</div>
<div class="w-1/3 flex items-start">
<div class="mt-2">
<!-- Adjusted margin class -->
<Button
href="/scripts/add?hub=hub%2F1088%2Fwindmill%2FGlobal_%2F_workspace_error_handler_template"
target="_blank">Use template</Button
>
</div>
</div>
<ErrorOrRecoveryHandler
isEditable={true}
handlersOnlyForEe={['slack']}
showScriptHelpText={true}
customInitialScriptPath={errorHandlerInitialScriptPath}
bind:handlerSelected={errorHandlerSelected}
bind:handlerPath={errorHandlerScriptPath}
slackHandlerScriptPath={slackErrorHandler}
customScriptTemplate="/scripts/add?hub=hub%2F2420%2Fwindmill%2Fworkspace_error_handler_template"
bind:customHandlerKind={errorHandlerItemKind}
bind:handlerExtraArgs={errorHandlerExtraArgs}
>
<svelte:fragment slot="custom-tab-tooltip">
<Tooltip>
<div class="flex gap-20 items-start mt-3">
<div class="text-sm">
The following args will be passed to the error handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that errored.</li>
<li><b>email</b>: The email of the user who ran the script or flow that errored.</li>
<li><b>error</b>: The error details.</li>
<li><b>job_id</b>: The job id.</li>
<li><b>is_flow</b>: Whether the error comes from a flow.</li>
<li><b>workspace_id</b>: The workspace id of the failed script or flow.</li>
</ul>
<br />
The error handler will be executed by the automatically created group g/error_handler. If
your error handler requires variables or resources, you need to add them to the group.
</div>
</div>
</Tooltip>
</svelte:fragment>
</ErrorOrRecoveryHandler>
<div class="flex mt-5 justify-start">
<Button
disabled={(errorHandlerSelected === 'slack' && !emptyString(errorHandlerScriptPath) && emptyString(errorHandlerExtraArgs['channel']))}
size="sm"
on:click={editErrorHandler}
>
Save
</Button>
</div>
{:else if tab == 'openai'}
<PageHeader title="Windmill AI" primary={false} />
<div class="mt-2">