From 725606db12e7c1850c040b23d5ec32d1985580a5 Mon Sep 17 00:00:00 2001 From: hcourdent Date: Thu, 14 Nov 2024 14:31:27 +0100 Subject: [PATCH] Feedback Ruben --- src/components/Pricing.js | 17 +- src/components/QuoteForm.tsx | 70 +++++---- src/components/pricing/PriceCalculator.js | 182 ++++++++++++++++++---- src/components/pricing/Slider.js | 2 +- 4 files changed, 201 insertions(+), 70 deletions(-) diff --git a/src/components/Pricing.js b/src/components/Pricing.js index 32393fe7..4a5b62d5 100644 --- a/src/components/Pricing.js +++ b/src/components/Pricing.js @@ -146,6 +146,11 @@ const pricing = { SLA & Priority Support 24/7 with 3h response time and engineer assistance ) }, + { + text: ( + Dedicated Slack or Discord channel + ) + }, { text: Design partners for roadmap } @@ -161,7 +166,12 @@ const pricing = { features: [ { text: 'No Audit logs' }, { text: 'No Distributed dependency cache backed by S3' }, - { text: 'Max 10 users with SSO' } + { text: 'Max 10 users with SSO' }, + { + text: ( + Max 10 compute units (CU) + ) + }, ] }, { text: 'Support with 48h response time by email' } @@ -211,7 +221,6 @@ const pricing = { { text: Everything in free }, - { text: Audit logs 7 days retention }, @@ -287,7 +296,9 @@ const pricing = { ) }, - + { + text: Dedicated Slack or Discord channel + }, { text: Design partners for roadmap } diff --git a/src/components/QuoteForm.tsx b/src/components/QuoteForm.tsx index 0f40c956..3979461d 100644 --- a/src/components/QuoteForm.tsx +++ b/src/components/QuoteForm.tsx @@ -1,6 +1,6 @@ import { Dialog } from '@headlessui/react'; import { Loader2, X } from 'lucide-react'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; export function QuoteForm({ workers, @@ -8,6 +8,7 @@ export function QuoteForm({ operators, frequency, plan, + total_price, open, setOpen, selectedOption @@ -145,73 +146,78 @@ export function QuoteForm({ ? 'Enterprise - Nonprofit' : 'Self-Hosted EE'} +
- Frequency - {frequency} + Price + ${total_price.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}/{frequency === 'monthly' ? 'mo' : 'yr'}
- Total seat units + Total seat units (SU) {Math.ceil(operators / 2) + developers}
{developers > 0 && (
- Developers -
- {developers} ({developers} {developers === 1 ? 'seat' : 'seats'}) + Developers +
+ {developers} + = + {developers} SU
)} {operators > 0 && (
- Operators -
- {operators} ({Math.ceil(operators / 2)} {Math.ceil(operators / 2) === 1 ? 'seat' : 'seats'}) + Operators +
+ {operators} + = + {Math.ceil(operators / 2)} SU
)}
- Total compute units + Total compute units (CU)
- {computeUnits} {computeUnits === 1 ? 'compute unit' : 'compute units'} + {computeUnits}
{workers.standard > 0 && (
- Standard workers -
- {workers.standard} - = - {workers.standard} CU + Standard workers +
+ {workers.standard} + = + {workers.standard} CU
)} {workers.small > 0 && (
- Small workers -
- {workers.small} - = - {Math.ceil(workers.small / 2)} CU + Small workers +
+ {workers.small} + = + {Math.ceil(workers.small / 2)} CU
)} {workers.large > 0 && (
- Large workers -
- {workers.large} - = - {workers.large * 2} CU + Large workers +
+ {workers.large} + = + {workers.large * 2} CU
)} {workers.native > 0 && (
- Native workers -
- {workers.native} - = - + Native workers +
+ {workers.native * 8} + = + {selectedOption === 'SMB' && plan === 'selfhosted_ee' ? Math.min(workers.native, 10) : workers.native} CU diff --git a/src/components/pricing/PriceCalculator.js b/src/components/pricing/PriceCalculator.js index b545f2a1..761b2ccd 100644 --- a/src/components/pricing/PriceCalculator.js +++ b/src/components/pricing/PriceCalculator.js @@ -216,39 +216,76 @@ export default function PriceCalculator({ period, tier, selectedOption }) { plan={tier.id === 'tier-enterprise-cloud' ? 'cloud_ee' : 'selfhosted_ee'} frequency={period.value === 'annually' ? 'yearly' : 'monthly'} selectedOption={selectedOption} + total_price={computeTotalPrice()} />

Price

- {(tier.id === 'tier-enterprise-selfhost' || tier.id === 'tier-enterprise-cloud') && - (workerGroups.reduce((sum, group) => { - const pricePerWorker = calculateWorkerPrice(group.memoryGB, tier.id, selectedOption) * - (tier.id === 'tier-enterprise-cloud' ? 2 : 1); - return sum + (pricePerWorker * group.workers); - }, 0) + (pricing.worker?.native * nativeWorkers / 8)) < - (tier.id === 'tier-enterprise-cloud' ? 200 : - (selectedOption === 'SMB' || selectedOption === 'Nonprofit' ? 40 : 100)) && ( - - Price for workers can't be below ${tier.id === 'tier-enterprise-cloud' - ? (period.value === 'annually' ? '2,000' : '200') - : (selectedOption === 'SMB' || selectedOption === 'Nonprofit' - ? (period.value === 'annually' ? '400' : '40') - : (period.value === 'annually' ? '1,000' : '100'))} - /{period.value === 'annually' ? 'yr' : 'mo'} - - )} + {(tier.id === 'tier-enterprise-selfhost' || tier.id === 'tier-enterprise-cloud') && ( + <> + {/* Existing minimum price warning */} + {(workerGroups.reduce((sum, group) => { + const pricePerWorker = calculateWorkerPrice(group.memoryGB, tier.id, selectedOption) * + (tier.id === 'tier-enterprise-cloud' ? 2 : 1); + return sum + (pricePerWorker * group.workers); + }, 0) + (pricing.worker?.native * nativeWorkers / 8)) < + (tier.id === 'tier-enterprise-cloud' ? 200 : + (selectedOption === 'SMB' || selectedOption === 'Nonprofit' ? 40 : 100)) && ( + + Price for workers can't be below ${tier.id === 'tier-enterprise-cloud' + ? (period.value === 'annually' ? '2,000' : '200') + : (selectedOption === 'SMB' || selectedOption === 'Nonprofit' + ? (period.value === 'annually' ? '400' : '40') + : (period.value === 'annually' ? '1,000' : '100'))} + /{period.value === 'annually' ? 'yr' : 'mo'} + + )} + + {/* New CU limit warning for SMB */} + {selectedOption === 'SMB' && ( + (() => { + const counts = getWorkerCounts(workerGroups); + const totalComputeUnits = (counts.small / 2 || 0) + + (counts.standard || 0) + + ((1/8) * nativeWorkers) + + (2 * (counts.large || 0)); + + return totalComputeUnits > 10 ? ( + + Maximum 10 CU on Pro plan, go to Enterprise if above + + ) : null; + })() + )} + + )}
- - {priceFormatter.format(computeTotalPrice())} - - - {period.value === 'annually' ? '/yr' : '/mo'} - + {(() => { + const counts = getWorkerCounts(workerGroups); + const totalComputeUnits = (counts.small / 2 || 0) + + (counts.standard || 0) + + ((1/8) * nativeWorkers) + + (2 * (counts.large || 0)); + const isOverLimit = selectedOption === 'SMB' && totalComputeUnits > 10; + const textColor = isOverLimit ? "text-rose-700 dark:text-red-400" : "text-gray-900 dark:text-white"; + const subTextColor = isOverLimit ? "text-rose-700 dark:text-red-400" : "text-gray-500"; + + return ( + <> + + {priceFormatter.format(computeTotalPrice())} + + + {period.value === 'annually' ? '/yr' : '/mo'} + + + ); + })()}
@@ -294,7 +331,15 @@ export default function PriceCalculator({ period, tier, selectedOption }) { {operators.toLocaleString()} - {' '}{operators <= 1 ? 'operator' : 'operators'} + {' '}{operators <= 1 ? 'operator' : 'operators'}{' '} + + + + + + An operator is a user who can only execute scripts, flows and apps, but not create and edit them. Operators are 1/2 price of developers (or 1/2 seats). + +
@@ -410,7 +455,7 @@ export default function PriceCalculator({ period, tier, selectedOption }) { @@ -421,11 +466,11 @@ export default function PriceCalculator({ period, tier, selectedOption }) {
Native workers{' '} - + - - Native workers are workers within the native worker group. This group is pre-configured to listen to native jobs tags (query languages). Those jobs are executed under a special mode with subworkers for increased throughput. You can set the number of native workers to 0. + + Native workers are workers within the native worker group. This group is pre-configured to listen to native jobs tags (query languages). Those jobs are executed under a special mode with subworkers for increased throughput. You can set the number of native workers to 0.
@@ -544,6 +589,29 @@ export default function PriceCalculator({ period, tier, selectedOption }) { )}
+
+ {(() => { + const counts = getWorkerCounts(workerGroups); + const totalComputeUnits = (counts.small / 2 || 0) + + (counts.standard || 0) + + ((1/8) * nativeWorkers) + + (2 * (counts.large || 0)); + const textColor = selectedOption === 'SMB' && totalComputeUnits > 10 + ? "text-rose-700 dark:text-red-400" + : "text-gray-900 dark:text-white"; + + return ( + <> + + Price: {priceFormatter.format(computeTotalPrice())} + + + /{period.value === 'annually' ? 'yr' : 'mo'} + + + ); + })()} +
) : null} @@ -551,7 +619,7 @@ export default function PriceCalculator({ period, tier, selectedOption }) { <>
Summary
-
+
Total seat units: {(developers + Math.ceil(operators/2)).toLocaleString()} @@ -568,7 +636,7 @@ export default function PriceCalculator({ period, tier, selectedOption }) { )}
-
+
{(() => { const counts = getWorkerCounts(workerGroups); const totalComputeUnits = (counts.small / 2 || 0) + @@ -579,7 +647,7 @@ export default function PriceCalculator({ period, tier, selectedOption }) { return ( <> - Total compute units: {Math.round(totalComputeUnits)} + Total compute units (CU): {Math.round(totalComputeUnits)} {counts.standard > 0 && ( {counts.standard} standard workers ={' '}{counts.standard} CU )} @@ -596,6 +664,29 @@ export default function PriceCalculator({ period, tier, selectedOption }) { ); })()}
+
+ {(() => { + const counts = getWorkerCounts(workerGroups); + const totalComputeUnits = (counts.small / 2 || 0) + + (counts.standard || 0) + + ((1/8) * nativeWorkers) + + (2 * (counts.large || 0)); + const textColor = selectedOption === 'SMB' && totalComputeUnits > 10 + ? "text-rose-700 dark:text-red-400" + : "text-gray-900 dark:text-white"; + + return ( + <> + + Price: {priceFormatter.format(computeTotalPrice())} + + + /{period.value === 'annually' ? 'yr' : 'mo'} + + + ); + })()} +
setShowQuoteForm(true)} @@ -613,7 +704,7 @@ export default function PriceCalculator({ period, tier, selectedOption }) { <>
Summary
-
+
Total seat units: {(developers + Math.ceil(operators/2)).toLocaleString()} @@ -630,7 +721,7 @@ export default function PriceCalculator({ period, tier, selectedOption }) { )}
-
+
{(() => { const counts = getWorkerCounts(workerGroups); const totalComputeUnits = (counts.small / 2 || 0) + @@ -641,7 +732,7 @@ export default function PriceCalculator({ period, tier, selectedOption }) { return ( <> - Total compute units: {Math.round(totalComputeUnits)} + Total compute units (CU): 10 ? "text-rose-700 dark:text-red-400" : ""}>{Math.round(totalComputeUnits)}{selectedOption === 'SMB' && totalComputeUnits > 10 ? ' (max 10 CU on Pro plan)' : ''} {counts.standard > 0 && ( {counts.standard} standard workers ={' '}{counts.standard} CU )} @@ -658,6 +749,29 @@ export default function PriceCalculator({ period, tier, selectedOption }) { ); })()}
+
+ {(() => { + const counts = getWorkerCounts(workerGroups); + const totalComputeUnits = (counts.small / 2 || 0) + + (counts.standard || 0) + + ((1/8) * nativeWorkers) + + (2 * (counts.large || 0)); + const textColor = selectedOption === 'SMB' && totalComputeUnits > 10 + ? "text-rose-700 dark:text-red-400" + : "text-gray-900 dark:text-white"; + + return ( + <> + + Price: {priceFormatter.format(computeTotalPrice())} + + + /{period.value === 'annually' ? 'yr' : 'mo'} + + + ); + })()} +
setShowQuoteForm(true)} diff --git a/src/components/pricing/Slider.js b/src/components/pricing/Slider.js index de9c63fc..e9bf750c 100644 --- a/src/components/pricing/Slider.js +++ b/src/components/pricing/Slider.js @@ -76,7 +76,7 @@ export default function Slider({ min, max, step, defaultValue, onChange, noExpon type="range" min={0} max={max} - step={isExponential ? (max / 1000) : step} // Make step size relative to max value + step={isExponential ? (max / 1000) : step} value={displayValue} onChange={handleChange} className="w-full h-2 appearance-none bg-gray-300 rounded-full outline-none accent-blue-500"