feat: http custom routes for static assets (#4666)

* feat: http custom routes for static assets

* update ee ref + fix build

* fix build

* nit
This commit is contained in:
HugoCasa
2024-11-07 20:16:14 +01:00
committed by GitHub
parent b1b760bbeb
commit dc8bd6d2b5
19 changed files with 402 additions and 88 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\" FROM http_trigger",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\" FROM http_trigger",
"describe": {
"columns": [
{
@@ -65,6 +65,11 @@
}
}
}
},
{
"ordinal": 10,
"name": "static_asset_config: _",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -80,8 +85,9 @@
false,
false,
false,
false
false,
true
]
},
"hash": "fe4f91ca7e179e58041a5c0b1a36015175e9301c01a99e8f2b82cb853349a183"
"hash": "11b698f82a54aac68b3617047dfe2b18dd6da7d962118fee276af354218baac2"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now())",
"query": "INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now())",
"describe": {
"columns": [],
"parameters": {
@@ -27,11 +27,12 @@
}
}
},
"Jsonb",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "c229744534f17f7622c3dee21bb1e7292ff17e6dffe58e80e53bff8baade07c8"
"hash": "333b484ffa030dee08e7b1161fcbc48af411377d2d9f58f92fc9d5eacdf0fba1"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, edited_by = $7, email = $8, is_async = $9, requires_auth = $10, edited_at = now() \n WHERE workspace_id = $11 AND path = $12",
"query": "UPDATE http_trigger \n SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now() \n WHERE workspace_id = $12 AND path = $13",
"describe": {
"columns": [],
"parameters": {
@@ -24,6 +24,7 @@
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
@@ -34,5 +35,5 @@
},
"nullable": []
},
"hash": "4d8640e84fccf1a0b799d8396a51e69345137e68d5096c70ba0a4332075d97ea"
"hash": "487d377e2df67fc3ea39d183ba9f99d45828d7c8e0ff10c5d74c454472e0493c"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, edited_by = $5, email = $6, is_async = $7, requires_auth = $8, edited_at = now() \n WHERE workspace_id = $9 AND path = $10",
"query": "UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now() \n WHERE workspace_id = $10 AND path = $11",
"describe": {
"columns": [],
"parameters": {
@@ -22,6 +22,7 @@
}
}
},
"Jsonb",
"Varchar",
"Varchar",
"Bool",
@@ -32,5 +33,5 @@
},
"nullable": []
},
"hash": "55d44f569f8ebfccddf975e1a330ef0dc286f4138efe923832371cdbac7157b0"
"hash": "7113d7cc72e44e4b7e01b69cc18cbe7b0399cf8ec0e9e6d2b05ceef589c432df"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\" FROM http_trigger WHERE workspace_id = $1",
"query": "SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\" FROM http_trigger WHERE workspace_id = $1",
"describe": {
"columns": [
{
@@ -65,6 +65,11 @@
}
}
}
},
{
"ordinal": 10,
"name": "static_asset_config: _",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -82,8 +87,9 @@
false,
false,
false,
false
false,
true
]
},
"hash": "02f1a6eeb27067dc438459238e7b016f5ccf9e3fe0ffbe88471f15aad8f74441"
"hash": "c9930fcfe79541af570eace58ba7e15a0816a6b4fd036cf7b991a210654b2633"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2",
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as \"http_method: _\", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as \"static_asset_config: _\"\n FROM http_trigger\n WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
@@ -80,6 +80,11 @@
"ordinal": 12,
"name": "requires_auth",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "static_asset_config: _",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -101,8 +106,9 @@
false,
false,
false,
false
false,
true
]
},
"hash": "4fb95eae1c871241efe2ef79615ce03cba0e4a12aad3274e4829d98e38ca1491"
"hash": "f904702536c106b0e5da8facae119c6af887c49a29ae44b3a95350ff27fb1ccf"
}
+1 -1
View File
@@ -1 +1 @@
aedaf655693cb9bab59fcf79a48c60aad6822668
6534b0f31fb4a75dd706fca2ce91e37e77e4ad02
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE http_trigger DROP COLUMN static_asset_config;
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE http_trigger ADD COLUMN static_asset_config JSONB;
+41
View File
@@ -9146,6 +9146,14 @@ paths:
in: query
schema:
type: string
- name: content_type
in: query
schema:
type: string
- name: content_disposition
in: query
schema:
type: string
requestBody:
description: File content
required: true
@@ -11463,6 +11471,17 @@ components:
type: string
route_path:
type: string
static_asset_config:
type: object
properties:
s3:
type: string
storage:
type: string
filename:
type: string
required:
- s3
is_flow:
type: boolean
extra_perms:
@@ -11509,6 +11528,17 @@ components:
type: string
route_path:
type: string
static_asset_config:
type: object
properties:
s3:
type: string
storage:
type: string
filename:
type: string
required:
- s3
is_flow:
type: boolean
http_method:
@@ -11542,6 +11572,17 @@ components:
type: string
route_path:
type: string
static_asset_config:
type: object
properties:
s3:
type: string
storage:
type: string
filename:
type: string
required:
- s3
is_flow:
type: boolean
http_method:
+129 -32
View File
@@ -1,24 +1,5 @@
use axum::{
extract::{Path, Query},
response::IntoResponse,
routing::{delete, get, post},
Extension, Json, Router,
};
use http::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::prelude::FromRow;
use std::collections::HashMap;
use tower_http::cors::CorsLayer;
use windmill_audit::{audit_ee::audit_log, ActionKind};
use windmill_common::{
db::UserDB,
error::{self, JsonResult},
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
use windmill_queue::PushArgsOwned;
#[cfg(feature = "parquet")]
use crate::job_helpers_ee::get_workspace_s3_resource;
use crate::{
db::{ApiAuthed, DB},
jobs::{
@@ -27,6 +8,31 @@ use crate::{
},
users::{fetch_api_authed, OptAuthed},
};
use axum::{
extract::{Path, Query},
response::IntoResponse,
routing::{delete, get, post},
Extension, Json, Router,
};
#[cfg(feature = "parquet")]
use http::header::IF_NONE_MATCH;
use http::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use sql_builder::{bind::Bind, SqlBuilder};
use sqlx::prelude::FromRow;
use std::collections::HashMap;
use tower_http::cors::CorsLayer;
use windmill_audit::{audit_ee::audit_log, ActionKind};
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::build_object_store_client;
use windmill_common::{
db::UserDB,
error::{self, JsonResult},
s3_helpers::S3Object,
utils::{not_found_if_none, paginate, require_admin, Pagination, StripPath},
worker::{to_raw_value, CLOUD_HOSTED},
};
use windmill_queue::PushArgsOwned;
lazy_static::lazy_static! {
static ref ROUTE_PATH_KEY_RE: regex::Regex = regex::Regex::new(r"/:\w+").unwrap();
@@ -99,6 +105,7 @@ struct NewTrigger {
is_async: bool,
requires_auth: bool,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
}
#[derive(FromRow, Serialize)]
@@ -116,6 +123,7 @@ struct Trigger {
is_async: bool,
requires_auth: bool,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
}
#[derive(Deserialize)]
@@ -127,6 +135,7 @@ struct EditTrigger {
is_async: bool,
requires_auth: bool,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
}
#[derive(Deserialize)]
@@ -182,7 +191,7 @@ async fn get_trigger(
let path = path.to_path();
let trigger = sqlx::query_as!(
Trigger,
r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth
r#"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, http_method as "http_method: _", edited_by, email, edited_at, extra_perms, is_async, requires_auth, static_asset_config as "static_asset_config: _"
FROM http_trigger
WHERE workspace_id = $1 AND path = $2"#,
w_id,
@@ -209,7 +218,7 @@ async fn create_trigger(
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now())",
"INSERT INTO http_trigger (workspace_id, path, route_path, route_path_key, script_path, is_flow, is_async, requires_auth, http_method, static_asset_config, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now())",
w_id,
ct.path,
ct.route_path,
@@ -218,7 +227,8 @@ async fn create_trigger(
ct.is_flow,
ct.is_async,
ct.requires_auth,
ct.http_method as HttpMethod,
ct.http_method as _,
ct.static_asset_config as _,
&authed.username,
&authed.email
)
@@ -261,14 +271,15 @@ async fn update_trigger(
sqlx::query!(
"UPDATE http_trigger
SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, edited_by = $7, email = $8, is_async = $9, requires_auth = $10, edited_at = now()
WHERE workspace_id = $11 AND path = $12",
SET route_path = $1, route_path_key = $2, script_path = $3, path = $4, is_flow = $5, http_method = $6, static_asset_config = $7, edited_by = $8, email = $9, is_async = $10, requires_auth = $11, edited_at = now()
WHERE workspace_id = $12 AND path = $13",
ct.route_path,
&route_path_key,
ct.script_path,
ct.path,
ct.is_flow,
ct.http_method as HttpMethod,
ct.http_method as _,
ct.static_asset_config as _,
&authed.username,
&authed.email,
ct.is_async,
@@ -279,12 +290,13 @@ async fn update_trigger(
.execute(&mut *tx).await?;
} else {
sqlx::query!(
"UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, edited_by = $5, email = $6, is_async = $7, requires_auth = $8, edited_at = now()
WHERE workspace_id = $9 AND path = $10",
"UPDATE http_trigger SET script_path = $1, path = $2, is_flow = $3, http_method = $4, static_asset_config = $5, edited_by = $6, email = $7, is_async = $8, requires_auth = $9, edited_at = now()
WHERE workspace_id = $10 AND path = $11",
ct.script_path,
ct.path,
ct.is_flow,
ct.http_method as HttpMethod,
ct.http_method as _,
ct.static_asset_config as _,
&authed.username,
&authed.email,
ct.is_async,
@@ -405,6 +417,7 @@ struct TriggerRoute {
edited_by: String,
email: String,
http_method: HttpMethod,
static_asset_config: Option<sqlx::types::Json<S3Object>>,
}
async fn get_http_route_trigger(
@@ -421,7 +434,7 @@ async fn get_http_route_trigger(
let route_path = StripPath(splitted.collect::<Vec<_>>().join("/"));
let triggers = sqlx::query_as!(
TriggerRoute,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _" FROM http_trigger WHERE workspace_id = $1"#,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _", static_asset_config as "static_asset_config: _" FROM http_trigger WHERE workspace_id = $1"#,
w_id
)
.fetch_all(db)
@@ -430,7 +443,7 @@ async fn get_http_route_trigger(
} else {
let triggers = sqlx::query_as!(
TriggerRoute,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _" FROM http_trigger"#,
r#"SELECT path, script_path, is_flow, route_path, workspace_id, is_async, requires_auth, edited_by, email, http_method as "http_method: _", static_asset_config as "static_asset_config: _" FROM http_trigger"#,
)
.fetch_all(db)
.await?;
@@ -518,6 +531,90 @@ async fn route_job(
Ok(trigger) => trigger,
Err(e) => return e.into_response(),
};
#[cfg(not(feature = "parquet"))]
if trigger.static_asset_config.is_some() {
return error::Error::InternalErr(
"Static asset configuration is not supported in this build".to_string(),
)
.into_response();
}
#[cfg(feature = "parquet")]
if let Some(sqlx::types::Json(config)) = trigger.static_asset_config {
let build_static_response_f = async {
let (_, s3_resource_opt) = get_workspace_s3_resource(
&authed,
&db,
None,
&"NO_TOKEN".to_string(), // no token is provided in this case
&trigger.workspace_id,
config.storage,
)
.await?;
let s3_resource = s3_resource_opt.ok_or(error::Error::InternalErr(
"No files storage resource defined at the workspace level".to_string(),
))?;
let s3_client = build_object_store_client(&s3_resource).await?;
let path = object_store::path::Path::from(config.s3);
let s3_object = s3_client.get(&path).await.map_err(|err| {
tracing::warn!("Error retrieving file from S3: {:?}", err);
error::Error::InternalErr(format!("Error retrieving file: {}", err.to_string()))
})?;
let mut response_headers = http::HeaderMap::new();
if let Some(ref e_tag) = s3_object.meta.e_tag {
if let Some(if_none_match) = headers.get(IF_NONE_MATCH) {
if if_none_match == e_tag {
return Ok::<_, error::Error>((
StatusCode::NOT_MODIFIED,
response_headers,
axum::body::Body::empty(),
));
}
}
if let Ok(e_tag) = e_tag.parse() {
response_headers.insert("etag", e_tag);
}
}
response_headers.insert(
"content-type",
s3_object
.attributes
.get(&object_store::Attribute::ContentType)
.map(|s| s.parse().ok())
.flatten()
.unwrap_or("application/octet-stream".parse().unwrap()),
);
response_headers.insert(
"content-disposition",
config.filename.as_ref().map_or_else(
|| {
s3_object
.attributes
.get(&object_store::Attribute::ContentDisposition)
.map(|s| s.parse().ok())
.flatten()
.unwrap_or("inline".parse().unwrap())
},
|filename| {
format!("inline; filename=\"{}\"", filename)
.parse()
.unwrap_or("inline".parse().unwrap())
},
),
);
let body_stream = axum::body::Body::from_stream(s3_object.into_stream());
Ok::<_, error::Error>((StatusCode::OK, response_headers, body_stream))
};
match build_static_response_f.await {
Ok((status, headers, body_stream)) => {
return (status, headers, body_stream).into_response()
}
Err(e) => return e.into_response(),
}
}
let headers = headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
@@ -1,5 +1,23 @@
use axum::Router;
#[cfg(feature = "parquet")]
use crate::db::{ApiAuthed, DB};
#[cfg(feature = "parquet")]
use windmill_common::{db::UserDB, s3_helpers::ObjectStoreResource};
pub fn workspaced_service() -> Router {
Router::new()
}
#[cfg(feature = "parquet")]
pub async fn get_workspace_s3_resource<'c>(
_authed: &ApiAuthed,
_db: &DB,
_user_db: Option<UserDB>,
_token: &str,
_w_id: &str,
_storage: Option<String>,
) -> windmill_common::error::Result<(Option<bool>, Option<ObjectStoreResource>)> {
// implementation is not open source
Ok((None, None))
}
+4 -1
View File
@@ -109,10 +109,13 @@ pub struct S3AwsOidcResource {
pub audience: Option<String>,
}
#[derive(Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone)]
pub struct S3Object {
pub s3: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
}
#[cfg(feature = "parquet")]
+1
View File
@@ -799,6 +799,7 @@ pub async fn get_cached_resource_value_if_valid(
S3Object {
s3: s3_file_key.clone(),
storage: cached_resource.storage.clone(),
filename: None,
},
)
.await;
@@ -299,7 +299,7 @@
let storage: string | undefined = undefined
export async function open(
preSelectedFileKey: { s3: string; storage: string | undefined } | undefined = undefined
preSelectedFileKey: { s3: string; storage?: string } | undefined = undefined
) {
storage = preSelectedFileKey?.storage
if (preSelectedFileKey !== undefined) {
@@ -4,6 +4,7 @@
import Button from '../../common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import type { ReadFileAs } from './model'
import { sendUserToast } from '$lib/toast'
type ConvertedFile = string | ArrayBuffer | null
@@ -89,7 +90,12 @@
event.preventDefault()
if (event.dataTransfer) {
if (event.dataTransfer.files && event.dataTransfer.files.length) {
onChange(event.dataTransfer.files)
if (!multiple && event.dataTransfer.files.length > 1) {
sendUserToast('Only one file can be uploaded at a time')
return
} else {
onChange(event.dataTransfer.files)
}
}
}
}
@@ -14,7 +14,9 @@
export let acceptedFileTypes: string[] | undefined = ['*']
export let allowMultiple: boolean = true
export let containerText: string = 'Drag and drop files here or click to browse'
export let containerText: string = allowMultiple
? 'Drag and drop files here or click to browse'
: 'Drag and drop a file here or click to browse'
export let customResourcePath: string | undefined = undefined
export let customResourceType: 's3' | 'azure_blob' | undefined = undefined // when customResourcePath is provided, this should be provided as well. Will default to S3 if not
export let customClass: string = ''
@@ -109,6 +111,10 @@
if (fileExtension) {
params.append('file_extension', fileExtension)
}
if (fileToUpload.type) {
params.append('content_type', fileToUpload.type)
}
// let response = await fetch(
// `/api/w/${$workspaceStore}/job_helpers/multipart_upload_s3_file?${params.toString()}`,
// {
@@ -400,7 +406,6 @@
accept={acceptedFileTypes?.join(',')}
multiple={allowMultiple}
returnFileNames
includeMimeType
on:change={({ detail }) => {
forceDisplayUploads = false
handleChange(detail)
@@ -10,13 +10,18 @@
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import Section from '$lib/components/Section.svelte'
import { Loader2, Save } from 'lucide-svelte'
import { Loader2, Save, Pipette } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import { page } from '$app/stores'
import { isCloudHosted } from '$lib/cloud'
import { base } from '$lib/base'
import S3FilePicker from '../S3FilePicker.svelte'
import Toggle from '../Toggle.svelte'
import JsonEditor from '../apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
import FileUpload from '../common/fileUpload/FileUpload.svelte'
import SimpleEditor from '../SimpleEditor.svelte'
let is_flow: boolean = false
let initialPath = ''
@@ -64,6 +69,8 @@
initialScriptPath = ''
fixedScriptPath = fixedScriptPath_ ?? ''
script_path = fixedScriptPath
static_asset_config = undefined
s3FileUploadRawMode = false
path = ''
initialPath = ''
dirtyPath = false
@@ -81,6 +88,12 @@
let initialRoutePath = ''
let route_path = ''
let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
undefined
let s3FilePicker: S3FilePicker
let s3FileUploadRawMode = false
let s3Editor: SimpleEditor | undefined = undefined
const dispatch = createEventDispatcher()
@@ -100,6 +113,8 @@
http_method = s.http_method ?? 'post'
is_async = s.is_async
requires_auth = s.requires_auth
static_asset_config = s.static_asset_config
s3FileUploadRawMode = !!static_asset_config
can_write = canWrite(s.path, s.extra_perms, $userStore)
}
@@ -116,7 +131,8 @@
is_async,
requires_auth,
route_path: $userStore?.is_admin || $userStore?.is_super_admin ? route_path : undefined,
http_method
http_method,
static_asset_config
}
})
sendUserToast(`Route ${path} updated`)
@@ -130,7 +146,8 @@
is_async,
requires_auth,
route_path,
http_method
http_method,
static_asset_config
}
})
sendUserToast(`Route ${path} created`)
@@ -181,6 +198,17 @@
$: validateRoute(route_path, http_method)
</script>
{#if static_asset_config}
<S3FilePicker
bind:this={s3FilePicker}
bind:selectedFileKey={static_asset_config}
on:close={() => {
s3Editor?.setCode(JSON.stringify(static_asset_config, null, 2))
}}
readOnlyMode={false}
/>
{/if}
<Drawer size="700px" bind:this={drawer}>
<DrawerContent
title={edit ? (can_write ? `Edit route ${initialPath}` : `Route ${initialPath}`) : 'New route'}
@@ -190,7 +218,11 @@
{#if !drawerLoading && can_write}
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' || routeError != '' || emptyString(script_path) || !can_write}
disabled={pathError != '' ||
routeError != '' ||
(!static_asset_config && emptyString(script_path)) ||
(static_asset_config && emptyString(static_asset_config.s3)) ||
!can_write}
on:click={triggerScript}
>
Save
@@ -286,35 +318,107 @@
</div>
</Section>
<Section label="Runnable">
<p class="text-xs mb-1 text-tertiary">
Pick a script or flow to be triggered<Required required={true} /><br />
To handle headers, query or path parameters, add a preprocessor to your runnable.
</p>
<div class="flex flex-row mb-2">
<ScriptPicker
disabled={fixedScriptPath != '' || !can_write}
initialPath={fixedScriptPath || initialScriptPath}
kinds={['script']}
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
allowRefresh
/>
<Section label="Target">
<ToggleButtonGroup
disabled={fixedScriptPath != '' || !can_write}
selected={static_asset_config ? 'static_asset' : 'runnable'}
on:selected={(ev) => {
if (ev.detail === 'static_asset') {
static_asset_config = { s3: '' }
script_path = ''
initialScriptPath = ''
is_flow = false
} else {
static_asset_config = undefined
}
}}
>
<ToggleButton label="Runnable" value="runnable" />
<ToggleButton label="Static asset" value="static_asset" />
</ToggleButtonGroup>
{#if script_path === undefined}
<Button
btnClasses="ml-4 mt-2"
color="dark"
{#if static_asset_config}
<div class="flex flex-col w-full gap-1">
<Toggle
class="flex justify-end"
bind:checked={s3FileUploadRawMode}
size="xs"
href={itemKind === 'flow'
? '/flows/add?hub=55'
: '/scripts/add?hub=hub%2F9088%2Fwindmill%2FHTTP%20route%20script%20with%20preprocessor%20template'}
target="_blank">Create from template</Button
>
{/if}
</div>
options={{ left: 'Existing file' }}
/>
{#if s3FileUploadRawMode}
<JsonEditor
bind:editor={s3Editor}
on:focus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
dispatch('blur')
}}
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
bind:value={static_asset_config}
/>
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
s3FilePicker?.open?.(static_asset_config)
}}
startIcon={{ icon: Pipette }}
>
Choose an object from the catalog
</Button>
{:else}
<FileUpload
allowMultiple={false}
randomFileKey={true}
on:addition={(evt) => {
static_asset_config = {
s3: evt.detail?.path ?? '',
filename: evt.detail?.filename ?? undefined
}
s3FileUploadRawMode = true
}}
on:deletion={(evt) => {
static_asset_config = {
s3: ''
}
}}
/>
{/if}
</div>
{:else}
<p class="text-xs mb-1 text-tertiary">
Pick a script or flow to be triggered<Required required={true} /><br />
To handle headers, query or path parameters, add a preprocessor to your runnable.
</p>
<div class="flex flex-row mb-2">
<ScriptPicker
disabled={fixedScriptPath != '' || !can_write}
initialPath={fixedScriptPath || initialScriptPath}
kinds={['script']}
allowFlow={true}
bind:itemKind
bind:scriptPath={script_path}
allowRefresh
/>
{#if script_path === undefined}
<Button
btnClasses="ml-4 mt-2"
color="dark"
size="xs"
href={itemKind === 'flow'
? '/flows/add?hub=55'
: '/scripts/add?hub=hub%2F9088%2Fwindmill%2FHTTP%20route%20script%20with%20preprocessor%20template'}
target="_blank">Create from template</Button
>
{/if}
</div>
{/if}
</Section>
<Section label="Settings">
<div class="flex flex-col gap-4">
<div class="flex flex-row justify-between">
+19 -5
View File
@@ -358,15 +358,15 @@ class Windmill:
"percent": value,
"flow_job_id": flow_id or None,
},
)
)
def get_progress(self, job_id: Optional[str] = None ) -> Any:
def get_progress(self, job_id: Optional[str] = None) -> Any:
workspace = get_workspace()
job_id = job_id or os.environ.get("WM_JOB_ID")
r = self.get(
f"/w/{workspace}/job_metrics/get_progress/{job_id}",
)
)
if r.status_code == 404:
print(f"Job {job_id} does not exist")
return None
@@ -472,7 +472,13 @@ class Windmill:
print(file_reader.read())
'''
"""
reader = S3BufferedReader(f"{self.workspace}", self.client, s3object["s3"], s3_resource_path, s3object["storage"] if "storage" in s3object else None)
reader = S3BufferedReader(
f"{self.workspace}",
self.client,
s3object["s3"],
s3_resource_path,
s3object["storage"] if "storage" in s3object else None,
)
return reader
def write_s3_file(
@@ -480,6 +486,8 @@ class Windmill:
s3object: S3Object | None,
file_content: BufferedReader | bytes,
s3_resource_path: str | None,
content_type: str | None,
content_disposition: str | None,
) -> S3Object:
"""
Write a file to the workspace S3 bucket
@@ -511,8 +519,12 @@ class Windmill:
query_params["file_key"] = s3object["s3"]
if s3_resource_path is not None and s3_resource_path != "":
query_params["s3_resource_path"] = s3_resource_path
if s3object is not None and "storage" in s3object and s3object["storage"] is not None:
if s3object is not None and "storage" in s3object and s3object["storage"] is not None:
query_params["storage"] = s3object["storage"]
if content_type is not None:
query_params["content_type"] = content_type
if content_disposition is not None:
query_params["content_disposition"] = content_disposition
try:
# need a vanilla client b/c content-type is not application/json here
@@ -865,6 +877,7 @@ def set_state(value: Any) -> None:
"""
return _client.set_state(value)
@init_global_client
def set_progress(value: int, job_id: Optional[str] = None) -> None:
"""
@@ -872,6 +885,7 @@ def set_progress(value: int, job_id: Optional[str] = None) -> None:
"""
return _client.set_progress(value, job_id)
@init_global_client
def get_progress(job_id: Optional[str] = None) -> Any:
"""