diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index f74f2fb1b0..cbb760811d 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -177,26 +177,6 @@ }, "query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'admins')" }, - "0473482de2910768dbe1cd4569cf157c91939150ea176f44bcce68eb68dbdeb9": { - "describe": { - "columns": [ - { - "name": "usage", - "ordinal": 0, - "type_info": "Int4" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Varchar" - ] - } - }, - "query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage" - }, "04a15674cf66f2822085e65ca33ee42b8e7f3f9d63ffac0585a12368d0322252": { "describe": { "columns": [ @@ -801,75 +781,6 @@ }, "query": "DELETE FROM queue WHERE schedule_path = $1 AND running = false AND job_kind = $2" }, - "1baf08ea78b84c9836c55975044d072c9d92400a1b1248a61d2d0247ac872fe4": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Uuid", - "Uuid", - "Varchar", - "Timestamptz", - "Timestamptz", - "Bool", - "Int8", - "Varchar", - "Jsonb", - "Jsonb", - "Text", - "Text", - "Text", - "Bool", - "Varchar", - "Text", - { - "Custom": { - "kind": { - "Enum": [ - "script", - "preview", - "flow", - "dependencies", - "flowpreview", - "script_hub", - "identity", - "flowdependencies" - ] - }, - "name": "job_kind" - } - }, - "Varchar", - "Varchar", - "Jsonb", - "Jsonb", - "Bool", - "Bool", - { - "Custom": { - "kind": { - "Enum": [ - "python3", - "deno", - "go", - "bash" - ] - }, - "name": "script_lang" - } - }, - "Numeric", - "Varchar", - "Bool", - "Int4", - "Varchar" - ] - } - }, - "query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email\n , visible_to_owner\n , mem_peak\n , tag\n )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29, $30)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)" - }, "1bf2ca894246bd716875635b2d0c294a1ce2ed21916097ea165df240f7421a1e": { "describe": { "columns": [], @@ -3721,26 +3632,6 @@ }, "query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5)" }, - "8af34c8fd96ccc3c007e211982e60306fee109944c2a053e8c38632992c39983": { - "describe": { - "columns": [ - { - "name": "usage", - "ordinal": 0, - "type_info": "Int4" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Varchar" - ] - } - }, - "query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage" - }, "8c0131a9cc61f2daa258d49767242bcaab6bb34a977ff7fb0c18aa9202d11f47": { "describe": { "columns": [], @@ -5351,6 +5242,27 @@ }, "query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE" }, + "bd086c56ad3a58163619122641b93b941ef75880eeeab0756d40f68f87760d5b": { + "describe": { + "columns": [ + { + "name": "usage", + "ordinal": 0, + "type_info": "Int4" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Varchar", + "Bool" + ] + } + }, + "query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, $2, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage" + }, "be1de9116b18a40681cb8c5ec7578fd7c5f7ce77f7af63f766c2899d85fe0bef": { "describe": { "columns": [ @@ -5762,6 +5674,83 @@ }, "query": "\n SELECT id, flow_status, suspend, script_path\n FROM queue\n WHERE id = ( SELECT parent_job FROM queue WHERE id = $1 UNION ALL SELECT parent_job FROM completed_job WHERE id = $1)\n FOR UPDATE\n " }, + "c8467b5a1a9c915384b7671990111ec548e399a44df6773f48a81d8956aab683": { + "describe": { + "columns": [ + { + "name": "duration_ms", + "ordinal": 0, + "type_info": "Int4" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Timestamptz", + "Timestamptz", + "Bool", + "Int8", + "Varchar", + "Jsonb", + "Jsonb", + "Text", + "Text", + "Text", + "Bool", + "Varchar", + "Text", + { + "Custom": { + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview", + "script_hub", + "identity", + "flowdependencies" + ] + }, + "name": "job_kind" + } + }, + "Varchar", + "Varchar", + "Jsonb", + "Jsonb", + "Bool", + "Bool", + { + "Custom": { + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash" + ] + }, + "name": "script_lang" + } + }, + "Numeric", + "Varchar", + "Bool", + "Int4", + "Varchar" + ] + } + }, + "query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email\n , visible_to_owner\n , mem_peak\n , tag\n )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29, $30)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12) RETURNING duration_ms" + }, "c9d97800eb0ec87df8e8959b283dacb2c6cce422365ed394375641488ceb6b65": { "describe": { "columns": [], @@ -6202,19 +6191,6 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1)" }, - "de4c8f42ff846599d6b2b26615a34f0bcbad974edc5f00fb0491475017eebaeb": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Int4" - ] - } - }, - "query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2" - }, "df8f9b2e601b0157759e9507308ea7df562a7a42516be0fcb465b8e34de0f438": { "describe": { "columns": [ @@ -6392,6 +6368,20 @@ }, "query": "DELETE FROM completed_job WHERE workspace_id = $1" }, + "e7418515b88d14d1fd79078342963e11422eed9a60e0472c68971f4e8f8735d9": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Bool", + "Int4" + ] + } + }, + "query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, $2, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $3" + }, "e838b15953b567af2a2da6a4e3a04add5ec2cacabd93171a97c5fc978a64f8aa": { "describe": { "columns": [], @@ -6839,19 +6829,6 @@ }, "query": "SELECT SUM(duration_ms) as duration FROM completed_job WHERE id = ANY($1)" }, - "f67ba6ca3ed802ca41e49a310a4161ea8d81f79021f11f3a9f2b65e93ce6312d": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Int4" - ] - } - }, - "query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2" - }, "f7f2598ac824e9b5719b96b1d1873ad728f7f786c7f447df6cac3e65d5a53bf1": { "describe": { "columns": [ diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index cbbb674bf6..2f3f5606fe 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -259,37 +259,15 @@ async fn stripe_checkout( "team" => Some(vec![ stripe::CreateCheckoutSessionLineItems { quantity: Some(1), - price: Some("price_1MUlrWGU3NdFi9eLE9GBZhoY".to_string()), + price: Some("price_1NCNOgGU3NdFi9eLuG4fZuEP".to_string()), ..Default::default() }, stripe::CreateCheckoutSessionLineItems { quantity: None, - price: Some("price_1MUlreGU3NdFi9eLi6sOyvVa".to_string()), - ..Default::default() - }, - stripe::CreateCheckoutSessionLineItems { - quantity: None, - price: Some("price_1MUlrlGU3NdFi9eLFLggSXZV".to_string()), - ..Default::default() - }, - stripe::CreateCheckoutSessionLineItems { - quantity: None, - price: Some("price_1MUlr3GU3NdFi9eLbZYFjR9p".to_string()), + price: Some("price_1NCNCpGU3NdFi9eLbiE6Ca42".to_string()), ..Default::default() }, ]), - // "enterprise" => Some(vec![ - // stripe::CreateCheckoutSessionLineItems { - // quantity: None, - // price: Some("price_1MSdf6GU3NdFi9eLJFRkntlx".to_string()), - // ..Default::default() - // }, - // stripe::CreateCheckoutSessionLineItems { - // quantity: None, - // price: Some("price_1MShsNGU3NdFi9eLJMEZUW8b".to_string()), - // ..Default::default() - // }, - // ]), _ => Err(Error::BadRequest("invalid plan".to_string()))?, }; params.customer_email = Some(&authed.email); diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 4af9af1480..23547f2079 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -351,7 +351,8 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>( Ulid::new().into() }; - if cfg!(feature = "enterprise") { + #[cfg(feature = "enterprise")] + { let premium_workspace = *CLOUD_HOSTED && sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", workspace_id) .fetch_one(&mut tx) @@ -365,27 +366,17 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>( job_payload, JobPayload::Flow(_) | JobPayload::RawFlow { .. } ) { - if !premium_workspace { - sqlx::query_scalar!( + sqlx::query_scalar!( "INSERT INTO usage (id, is_workspace, month_, usage) - VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) + VALUES ($1, $2, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 RETURNING usage.usage", - email) + if premium_workspace { workspace_id } else { email }, + premium_workspace + ) .fetch_one(&mut tx) .await .map_err(|e| Error::InternalErr(format!("updating usage: {e}")))? - } else { - sqlx::query_scalar!( - "INSERT INTO usage (id, is_workspace, month_, usage) - VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) - ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 - RETURNING usage.usage", - workspace_id) - .fetch_one(&mut tx) - .await - .map_err(|e| Error::InternalErr(format!("updating usage: {e}")))? - } } else if *CLOUD_HOSTED && !premium_workspace { sqlx::query_scalar!( " diff --git a/backend/windmill-worker/src/jobs.rs b/backend/windmill-worker/src/jobs.rs index 23a80f5c4f..e37d5b8410 100644 --- a/backend/windmill-worker/src/jobs.rs +++ b/backend/windmill-worker/src/jobs.rs @@ -67,29 +67,30 @@ pub async fn add_completed_job( logs: String, rsmq: Option, ) -> Result { - let duration = - if queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview { - let jobs = queued_job.parse_flow_status().map(|s| { - let mut modules = s.modules; - modules.extend([s.failure_module.module_status]); - flatten_jobs(modules) - }); - if let Some(jobs) = jobs { - sqlx::query_scalar!( - "SELECT SUM(duration_ms) as duration FROM completed_job WHERE id = ANY($1)", - jobs.as_slice() - ) - .fetch_one(db) - .await - .ok() - .flatten() - } else { - tracing::warn!("Could not parse flow status"); - None - } + let is_flow = + queued_job.job_kind == JobKind::Flow || queued_job.job_kind == JobKind::FlowPreview; + let duration = if is_flow { + let jobs = queued_job.parse_flow_status().map(|s| { + let mut modules = s.modules; + modules.extend([s.failure_module.module_status]); + flatten_jobs(modules) + }); + if let Some(jobs) = jobs { + sqlx::query_scalar!( + "SELECT SUM(duration_ms) as duration FROM completed_job WHERE id = ANY($1)", + jobs.as_slice() + ) + .fetch_one(db) + .await + .ok() + .flatten() } else { + tracing::warn!("Could not parse flow status"); None - }; + } + } else { + None + }; let mem_peak = sqlx::query_scalar!("SELECT mem_peak FROM queue WHERE id = $1", &queued_job.id) .fetch_optional(db) @@ -99,7 +100,7 @@ pub async fn add_completed_job( .flatten(); let mut tx: QueueTransaction<'_, R> = (rsmq, db.begin().await?).into(); let job_id = queued_job.id.clone(); - sqlx::query!( + let duration = sqlx::query_scalar!( "INSERT INTO completed_job AS cj ( workspace_id , id @@ -134,7 +135,7 @@ pub async fn add_completed_job( ) VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,\ $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29, $30) - ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)", + ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12) RETURNING duration_ms", queued_job.workspace_id, queued_job.id, queued_job.parent_job, @@ -166,7 +167,7 @@ pub async fn add_completed_job( mem_peak, queued_job.tag, ) - .execute(&mut tx) + .fetch_one(&mut tx) .await .map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?; @@ -190,36 +191,25 @@ pub async fn add_completed_job( } tx.commit().await?; - if cfg!(enterprise) && duration.unwrap_or(0) > 1000 { - let additional_usage = duration.unwrap() as i32 / 1000; - + #[cfg(feature = "enterprise")] + if !is_flow && duration > 1000 { + let additional_usage = duration / 1000; let w_id = &queued_job.workspace_id; let premium_workspace = *CLOUD_HOSTED && sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", w_id) .fetch_one(db) .await .map_err(|e| Error::InternalErr(format!("fetching if {w_id} is premium: {e}")))?; - if premium_workspace { - let _ = sqlx::query!( + let _ = sqlx::query!( "INSERT INTO usage (id, is_workspace, month_, usage) - VALUES ($1, true, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) - ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2", - w_id, + VALUES ($1, $2, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) + ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $3", + if premium_workspace { w_id } else { &queued_job.email }, + premium_workspace, additional_usage) .execute(db) .await .map_err(|e| Error::InternalErr(format!("updating usage: {e}"))); - } else { - sqlx::query!( - "INSERT INTO usage (id, is_workspace, month_, usage) - VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) - ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + $2", - queued_job.email, - additional_usage) - .execute(db) - .await - .map_err(|e| Error::InternalErr(format!("updating usage: {e}")))?; - } } tracing::debug!("Added completed job {}", queued_job.id); diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 23e812dbe2..5378099fae 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -169,26 +169,18 @@ '1 000 free global executions per-user per month' ], Team: [ - `$10/mo, includes 1 user/author + 10k computations`, - `+ $8/mo per extra user/author in the workspace (1 included)`, - `+ $4/mo per extra operators in the workspace.`, - `$0.001 per additional computation (10k included)`, - `Google/Github/Microsoft/Gitlab SSO`, - `Unlimited variables/resources/scripts/apps/flows`, - `Support 24/7 with 48h response time` + `$10/mo per seat`, + `Every seat includes 10 000 executions`, + `Every seat includes either 1 user OR 2 operators` ], Enterprise: [ - `$200/mo, includes 1 user/author + 10k computations`, - `+ $32/mo per extra user/author in the workspace (1 included)`, - `+ $16/mo per extra operators in the workspace.`, - `$0.004 per additional computation (10k included)`, - `Dedicated and isolated database and workers available for +400$/mo (EU/US)`, - `Dedicated entire cluster available for +4000$/mo (EU/US)`, - `SAML support`, + `Dedicated and isolated database and workers available (EU/US/Asia)`, + `Dedicated entire cluster available for (EU/US/Asia)`, + `SAML support with group syncing`, `SLA`, `Priority Support 24/7 with 3h response time and automation engineer assistance`, `Design partners for Roadmap`, - `
(Self-hosted enterprise licenses also available at 50% discount)
` + `
Self-hosted licenses also available
` ] } @@ -485,29 +477,33 @@ {/if} {#if plan} - {@const team_factor = plan == 'team' ? 8 : 32} - {@const user_nb = Math.max(0, (users?.filter((x) => !x.operator).length ?? 0) - 1)} + {@const team_factor = plan == 'team' ? 10 : 40} + {@const user_nb = users?.filter((x) => !x.operator)?.length ?? 0} + {@const operator_nb = users?.filter((x) => x.operator)?.length ?? 0} + {@const seats_from_users = Math.ceil(user_nb + operator_nb / 2)} + {@const seats_from_comps = Math.ceil((premium_info?.usage ?? 0) / 10000)}
- Extra seats: -
{user_nb} * ${team_factor} - = ${(users?.length ?? 0) * team_factor}/mo
+ Authors: +
{user_nb}
Actual pricing is calculated on the MAXIMUM number of users in a given billing period, see the customer portal for more info.
- Extra operators: -
{Math.max(0, (users?.filter((x) => x.operator).length ?? 0) - 1)} * ${team_factor / - 2} = ${((users?.length ?? 0) * team_factor) / 2}/mo
+ Operators: +
{operator_nb}
Actual pricing is calculated on the MAXIMUM number of users in a given billing - period, see the customer portal for more info.Actual pricing is calculated on the MAXIMUM number of operators in a given + billing period, see the customer portal for more info. +
+ +
+ Seats from authors + operators: +
ceil({user_nb} + {operator_nb}/2) = {seats_from_users}
@@ -517,12 +513,19 @@
- Extra computations (above the 10k included) this month: -
{Math.max(0, (premium_info?.usage ?? 0) - 10000)} x {plan == 'team' - ? '0.001' - : '0.004'} = ${Math.max(0, (premium_info?.usage ?? 0) - 10000) * - (plan == 'team' ? 0.001 : 0.004)} + Seats from computations: +
ceil({premium_info?.usage ?? 0} / 10 000) = {seats_from_comps}
+
+ +
+ Total seats: +
+ max({seats_from_comps}, {seats_from_users}) * {team_factor} = ${Math.max( + seats_from_comps, + seats_from_users + ) * team_factor}/mo
{/if} @@ -534,7 +537,7 @@ {/if}
-
+
The single credit-unit is called a "computation". A computation corresponds to a @@ -556,7 +559,7 @@ {#each Object.entries(plans) as [planTitle, planDesc]}

{planTitle}

-
    +
      {#each planDesc as item}
    • {@html item}
    • {/each} @@ -566,7 +569,7 @@ {#if planTitle == 'Team'} {#if plan != 'team'}
      -
      @@ -576,11 +579,8 @@ {:else if planTitle == 'Enterprise'} {#if plan != 'enterprise'}
      - See more
      {:else}