feat: Add override all schedule handlers button (#2579)

* feat: Add override all schedule handlers button

* sqlx prepare

* sqlx prepare again
This commit is contained in:
Guillaume Bouvignies
2023-11-07 13:29:23 +01:00
committed by GitHub
parent 18d04220e6
commit f2bff84502
17 changed files with 297 additions and 62 deletions
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -67,7 +67,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -28,7 +28,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET ws_error_handler_muted = $1, on_failure = $2, on_failure_extra_args = $3, on_failure_times = $4, on_failure_exact = $5 WHERE workspace_id = $6",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Varchar",
"Json",
"Int4",
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "539d9ae486c7d1f5fb8c7278d698675b04359182b781b0cb0164402b798a7fef"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE schedule SET on_recovery = $1, on_recovery_extra_args = $2, on_recovery_times = $3 WHERE workspace_id = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Json",
"Int4",
"Text"
]
},
"nullable": []
},
"hash": "542ebd3d6cd8522d117112ef9eb13ff95b93d67994823a4a84d37639eb17e0b9"
}
@@ -60,7 +60,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -40,7 +40,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -46,7 +46,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -37,7 +37,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
+57 -1
View File
@@ -1,6 +1,6 @@
openapi: 3.0.3
info:
version: 1.200.0
version: 1.201.0
title: Windmill API
contact:
name: Windmill Team
@@ -875,6 +875,19 @@ paths:
'text/plain:':
schema:
type: string
/settings/send_stats:
post:
summary: send stats
operationId: sendStats
tags:
- setting
responses:
'200':
description: status
content:
'text/plain:':
schema:
type: string
/users/email:
get:
summary: get current user email (if logged in)
@@ -8150,6 +8163,49 @@ paths:
- id
- success
- duration_ms
/w/{workspace}/schedules/setdefaulthandler:
post:
summary: Set default error or recoevery handler
operationId: setDefaultErrorOrRecoveryHandler
tags:
- schedule
parameters:
- name: workspace
in: path
required: true
schema: *ref_0
requestBody:
description: Handler description
required: true
content:
application/json:
schema:
type: object
properties:
handler_type:
type: string
enum:
- error
- recovery
override_existing:
type: boolean
path:
type: string
extra_args:
type: object
number_of_occurence:
type: integer
number_of_occurence_exact:
type: boolean
workspace_handler_muted:
type: boolean
required:
- handler_type
- override_existing
- path
responses:
'201':
description: default error handler set
/groups/list:
get:
summary: list instance groups
+39
View File
@@ -5298,6 +5298,45 @@ paths:
items:
$ref: "#/components/schemas/ScheduleWJobs"
/w/{workspace}/schedules/setdefaulthandler:
post:
summary: Set default error or recoevery handler
operationId: setDefaultErrorOrRecoveryHandler
tags:
- schedule
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: Handler description
required: true
content:
application/json:
schema:
type: object
properties:
handler_type:
type: string
enum: ["error", "recovery"]
override_existing:
type: boolean
path:
type: string
extra_args:
type: object
number_of_occurence:
type: integer
number_of_occurence_exact:
type: boolean
workspace_handler_muted:
type: boolean
required:
- handler_type
- override_existing
- path
responses:
"201":
description: default error handler set
/groups/list:
get:
summary: list instance groups
+86
View File
@@ -8,7 +8,9 @@
use crate::{
db::{ApiAuthed, DB},
settings::set_global_setting_internal,
users::maybe_refresh_folders,
utils::require_super_admin,
};
use axum::{
extract::{Extension, Path, Query},
@@ -40,6 +42,7 @@ pub fn workspaced_service() -> Router {
.route("/update/*path", post(edit_schedule))
.route("/delete/*path", delete(delete_schedule))
.route("/setenabled/*path", post(set_enabled))
.route("/setdefaulthandler", post(set_default_error_handler))
}
pub fn global_service() -> Router {
@@ -65,6 +68,18 @@ pub struct NewSchedule {
pub ws_error_handler_muted: Option<bool>,
}
#[derive(Serialize, Deserialize)]
pub struct ErrorOrRecoveryHandler {
pub handler_type: String, // 'error' or 'recovery'
pub override_existing: bool,
pub path: String,
pub extra_args: Option<serde_json::Value>,
pub number_of_occurence: Option<i32>,
pub number_of_occurence_exact: Option<bool>,
pub workspace_handler_muted: Option<bool>,
}
async fn check_path_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
@@ -486,6 +501,77 @@ async fn delete_schedule(
Ok(format!("schedule {} deleted", path))
}
async fn set_default_error_handler(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(payload): Json<ErrorOrRecoveryHandler>,
) -> Result<()> {
require_super_admin(&db, &authed.email).await?;
let (key, value) = match payload.handler_type.as_str() {
"error" => {
let key = format!("default_error_handler_{}", w_id);
let value = serde_json::json!({
"wsErrorHandlerMuted": payload.workspace_handler_muted,
"errorHandlerPath": payload.path,
"errorHandlerExtraArgs": payload.extra_args,
"failedTimes": payload.number_of_occurence,
"failedExact": payload.number_of_occurence_exact,
});
Ok((key, value))
}
"recovery" => {
let key = format!("default_recovery_handler_{}", w_id);
let value = serde_json::json!({
"recoveryHandlerPath": payload.path,
"recoveryHandlerExtraArgs": payload.extra_args,
"recoveredTimes": payload.number_of_occurence,
});
Ok((key, value))
}
_ => Err(Error::BadRequest(
"handler_type must be either 'error' or 'recovery'".to_string(),
)),
}?;
set_global_setting_internal(&db, key, value).await?;
if payload.override_existing {
match payload.handler_type.as_str() {
"error" => {
sqlx::query!(
"UPDATE schedule SET ws_error_handler_muted = $1, on_failure = $2, on_failure_extra_args = $3, on_failure_times = $4, on_failure_exact = $5 WHERE workspace_id = $6",
payload.workspace_handler_muted,
payload.path,
payload.extra_args,
payload.number_of_occurence,
payload.number_of_occurence_exact,
w_id,
)
.execute(&db)
.await?;
Ok(())
}
"recovery" => {
sqlx::query!(
"UPDATE schedule SET on_recovery = $1, on_recovery_extra_args = $2, on_recovery_times = $3 WHERE workspace_id = $4",
payload.path,
payload.extra_args,
payload.number_of_occurence,
w_id,
)
.execute(&db)
.await?;
Ok(())
}
_ => Err(Error::BadRequest(
"handler_type must be either 'error' or 'recovery'".to_string(),
)),
}?;
}
Ok(())
}
async fn check_flow_conflict<'c>(
tx: &mut Transaction<'c, Postgres>,
w_id: &str,
+17 -10
View File
@@ -125,25 +125,32 @@ pub async fn set_global_setting(
Json(value): Json<Value>,
) -> error::Result<()> {
require_super_admin(&db, &authed.email).await?;
match value.value {
set_global_setting_internal(&db, key, value.value).await
}
pub async fn set_global_setting_internal(
db: &DB,
key: String,
value: serde_json::Value,
) -> error::Result<()> {
match value {
serde_json::Value::Null => {
delete_global_setting(&db, &key).await?;
delete_global_setting(db, &key).await?;
}
serde_json::Value::String(x) if x.is_empty() => {
delete_global_setting(&db, &key).await?;
delete_global_setting(db, &key).await?;
}
v => {
sqlx::query!(
"INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()",
key,
v
)
.execute(&db)
.await?;
"INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()",
key,
v
)
.execute(db)
.await?;
tracing::info!("Set global setting {} to {}", key, v);
}
};
Ok(())
}
-6
View File
@@ -1468,12 +1468,6 @@ pub fn is_none_or_false(val: &Option<bool>) -> bool {
}
}
pub fn is_none_or_empty(val: &Option<bool>) -> bool {
match val {
Some(val) => !val,
None => true,
}
}
enum ArchiveImpl {
Zip(async_zip::write::ZipFileWriter<File>),
Tar(tokio_tar::Builder<File>),
@@ -10,6 +10,7 @@
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import Dropdown from '$lib/components/Dropdown.svelte'
import {
FlowService,
ScheduleService,
@@ -21,6 +22,7 @@
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, formatCron, sendUserToast } from '$lib/utils'
import { faList, faSave } from '@fortawesome/free-solid-svg-icons'
import { Save } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Section from '$lib/components/Section.svelte'
@@ -164,42 +166,42 @@
}
}
async function saveAsDefaultErrorHandler() {
async function saveAsDefaultErrorHandler(overrideExisting: boolean) {
if (!$enterpriseLicense) {
sendUserToast(`Setting default error handler is an enterprise edition feature`, true)
return
}
if ($workspaceStore && errorHandlerPath !== undefined) {
await SettingService.setGlobal({
key: 'default_error_handler_' + $workspaceStore!,
await ScheduleService.setDefaultErrorOrRecoveryHandler({
workspace: $workspaceStore!,
requestBody: {
value: {
wsErrorHandlerMuted: wsErrorHandlerMuted,
errorHandlerPath: `${errorHandleritemKind}/${errorHandlerPath}`,
errorHandlerExtraArgs: errorHandlerExtraArgs,
failedTimes: failedTimes,
failedExact: failedExact
}
handler_type: 'error',
override_existing: overrideExisting,
path: `${errorHandleritemKind}/${errorHandlerPath}`,
extra_args: errorHandlerExtraArgs,
number_of_occurence: failedTimes,
number_of_occurence_exact: failedExact,
workspace_handler_muted: wsErrorHandlerMuted
}
})
sendUserToast(`Default error handler saved to ${errorHandlerPath}`, false)
}
}
async function saveAsDefaultRecoveryHandler() {
async function saveAsDefaultRecoveryHandler(overrideExisting: boolean) {
if (!$enterpriseLicense) {
sendUserToast(`Setting default recovery handler is an enterprise edition feature`, true)
return
}
if ($workspaceStore && errorHandlerPath !== undefined) {
await SettingService.setGlobal({
key: 'default_recovery_handler_' + $workspaceStore!,
await ScheduleService.setDefaultErrorOrRecoveryHandler({
workspace: $workspaceStore!,
requestBody: {
value: {
recoveryHandlerPath: `${recoveryHandlerItemKind}/${recoveryHandlerPath}`,
recoveryHandlerExtraArgs: recoveryHandlerExtraArgs,
recoveredTimes: recoveredTimes
}
handler_type: 'recovery',
override_existing: overrideExisting,
path: `${recoveryHandlerItemKind}/${recoveryHandlerPath}`,
extra_args: recoveryHandlerExtraArgs,
number_of_occurence: recoveredTimes
}
})
sendUserToast(`Default recovery handler saved to ${errorHandlerPath}`, false)
@@ -430,16 +432,28 @@
<Section label="Error handler">
<svelte:fragment slot="action">
<div class="flex flex-row items-center gap-2">
<Button
disabled={emptyString(errorHandlerPath)}
btnClasses="text-center"
color="light"
size="xs"
startIcon={{ icon: faSave }}
on:click={saveAsDefaultErrorHandler}
<Dropdown
placement="bottom-end"
name="Save as default"
dropdownItems={[
{
disabled: emptyString(errorHandlerPath),
displayName: `Future schedules only`,
action: () => saveAsDefaultErrorHandler(false)
},
{
disabled: emptyString(errorHandlerPath),
displayName: 'Override all existing',
type: 'delete',
action: () => saveAsDefaultErrorHandler(true)
}
]}
>
Save as default
</Button>
<svelte:fragment>
<Save size={12} class="mr-1" />
Set as default
</svelte:fragment>
</Dropdown>
</div>
</svelte:fragment>
<div class="flex flex-row">
@@ -519,16 +533,28 @@
</svelte:fragment>
<svelte:fragment slot="action">
<div class="flex flex-row items-center gap-2">
<Button
disabled={emptyString(recoveryHandlerPath)}
btnClasses="text-center"
color="light"
size="xs"
startIcon={{ icon: faSave }}
on:click={saveAsDefaultRecoveryHandler}
<Dropdown
placement="bottom-end"
name="Save as default"
dropdownItems={[
{
disabled: emptyString(errorHandlerPath),
displayName: `Future schedules only`,
action: () => saveAsDefaultRecoveryHandler(false)
},
{
disabled: emptyString(errorHandlerPath),
displayName: 'Override all existing',
type: 'delete',
action: () => saveAsDefaultRecoveryHandler(true)
}
]}
>
Save as default
</Button>
<svelte:fragment>
<Save size={12} class="mr-1" />
Set as default
</svelte:fragment>
</Dropdown>
</div>
</svelte:fragment>