Show warnings about concurrency and runs on the plans page

This commit is contained in:
Matt Aitken
2023-12-05 14:35:10 +00:00
parent 0b40dddd9a
commit be57dbb933
3 changed files with 88 additions and 18 deletions
@@ -1,9 +1,10 @@
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
import { LinkButton } from "../primitives/Buttons";
import { Paragraph } from "../primitives/Paragraph";
import { plansPath, organizationBillingPath } from "~/utils/pathBuilder";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { formatNumberCompact } from "~/utils/numberFormatter";
import { plansPath } from "~/utils/pathBuilder";
import { LinkButton } from "../primitives/Buttons";
import { Paragraph } from "../primitives/Paragraph";
type UpgradePromptProps = {
organization: MatchedOrganization;
@@ -12,14 +13,15 @@ type UpgradePromptProps = {
export function UpgradePrompt({ organization }: UpgradePromptProps) {
const currentPlan = useCurrentPlan();
if (!currentPlan || !currentPlan.usage.exceededRunCount) {
if (!currentPlan || !currentPlan.usage.exceededRunCount || !currentPlan.usage.runCountCap) {
return null;
}
return (
<div className="flex h-full w-full items-center gap-4 bg-gradient-to-r from-transparent to-indigo-900/50 pr-1.5">
<Paragraph variant="extra-small" className="text-rose-500">
You have exceeded the monthly {currentPlan.usage.runCountCap} Runs limit
You have exceeded the monthly {formatNumberCompact(currentPlan.usage.runCountCap)} Runs
limit
</Paragraph>
<LinkButton
variant={"primary/small"}
@@ -9,8 +9,52 @@ export class OrgBillingPlanPresenter {
this.#prismaClient = prismaClient;
}
public async call({ slug }: { slug: string }) {
const billingPresenter = new BillingService(true);
return billingPresenter.getPlans();
public async call({ slug, isManagedCloud }: { slug: string; isManagedCloud: boolean }) {
const billingPresenter = new BillingService(isManagedCloud);
const plans = await billingPresenter.getPlans();
if (plans === undefined) {
return;
}
const organization = await this.#prismaClient.organization.findFirst({
where: {
slug,
},
});
if (!organization) {
return;
}
const maxConcurrency = await this.#prismaClient.$queryRaw<
{ organization_id: string; max_concurrent_runs: BigInt }[]
>`WITH events AS (
SELECT
re.event_time,
re.organization_id,
re.event_type,
SUM(re.event_type) OVER (PARTITION BY re.organization_id ORDER BY re.event_time) AS running_total
FROM
triggerdotdev_events.run_executions re
WHERE
re.organization_id = ${organization.id}
AND re.event_time >= DATE_TRUNC('month',
CURRENT_DATE)
)
SELECT
organization_id, MAX(running_total) AS max_concurrent_runs
FROM
events
GROUP BY
organization_id;`;
return {
plans,
maxConcurrency:
maxConcurrency.at(0) !== undefined
? Number(maxConcurrency[0].max_concurrent_runs)
: undefined,
};
}
}
@@ -11,9 +11,12 @@ import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
import { Callout } from "~/components/primitives/Callout";
import { Header2 } from "~/components/primitives/Headers";
import { featuresForRequest } from "~/features.server";
import { useFeatures } from "~/hooks/useFeatures";
import { OrgBillingPlanPresenter } from "~/presenters/OrgBillingPlanPresenter";
import { Handle } from "~/utils/handle";
import { OrganizationParamsSchema, organizationBillingPath } from "~/utils/pathBuilder";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { formatNumberCompact } from "~/utils/numberFormatter";
export async function loader({ params, request }: LoaderFunctionArgs) {
const { organizationSlug } = OrganizationParamsSchema.parse(params);
@@ -24,12 +27,16 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
}
const presenter = new OrgBillingPlanPresenter();
const plans = await presenter.call({ slug: organizationSlug });
if (!plans) {
const result = await presenter.call({ slug: organizationSlug, isManagedCloud });
if (!result) {
throw new Response(null, { status: 404 });
}
return typedjson({ plans, organizationSlug });
return typedjson({
plans: result.plans,
maxConcurrency: result.maxConcurrency,
organizationSlug,
});
}
export const handle: Handle = {
@@ -37,16 +44,33 @@ export const handle: Handle = {
};
export default function Page() {
const { plans, organizationSlug } = useTypedLoaderData<typeof loader>();
const { plans, maxConcurrency, organizationSlug } = useTypedLoaderData<typeof loader>();
const currentPlan = useCurrentPlan();
const hitConcurrencyLimit =
currentPlan?.subscription?.limits.concurrentRuns && maxConcurrency
? maxConcurrency >= currentPlan.subscription!.limits.concurrentRuns!
: false;
const hitRunLimit = currentPlan?.usage?.runCountCap
? currentPlan.usage.currentRunCount > currentPlan.usage.runCountCap
: false;
return (
<div className="flex flex-col gap-4">
<Callout variant={"pricing"}>
Some of your Runs are being queued because your Run concurrency is limited to 50.
</Callout>
<Callout variant={"pricing"}>
You have exceeded the monthly 10,000 Runs limit. Upgrade to a paid plan before Nov 30.
</Callout>
{hitConcurrencyLimit && (
<Callout variant={"pricing"}>
Some of your Runs are being queued because your Run concurrency is limited to{" "}
{currentPlan?.subscription?.limits.concurrentRuns}.
</Callout>
)}
{hitRunLimit && (
<Callout variant={"pricing"}>
{`You have exceeded the monthly
${formatNumberCompact(currentPlan!.subscription!.limits.runs!)} Runs limit. Upgrade so you
can continue to perform Runs.`}
</Callout>
)}
<PricingTiers organizationSlug={organizationSlug} plans={plans} />
<div>
<Header2 spacing>Estimate your usage</Header2>