diff --git a/apps/webapp/app/components/billing/UpgradePrompt.tsx b/apps/webapp/app/components/billing/UpgradePrompt.tsx index 59f685778..6c7d1a3cb 100644 --- a/apps/webapp/app/components/billing/UpgradePrompt.tsx +++ b/apps/webapp/app/components/billing/UpgradePrompt.tsx @@ -1,7 +1,7 @@ import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { LinkButton } from "../primitives/Buttons"; import { Paragraph } from "../primitives/Paragraph"; -import { organizationBillingPath } from "~/utils/pathBuilder"; +import { PlansPath, organizationBillingPath } from "~/utils/pathBuilder"; import { MatchedOrganization } from "~/hooks/useOrganizations"; type UpgradePromptProps = { @@ -18,7 +18,7 @@ export function UpgradePrompt({ organization }: UpgradePromptProps) { variant={"primary/small"} LeadingIcon={ArrowUpCircleIcon} leadingIconClassName="px-0" - to={organizationBillingPath(organization)} + to={PlansPath(organization)} > Upgrade diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing._index/route.tsx new file mode 100644 index 000000000..2254ae6a4 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing._index/route.tsx @@ -0,0 +1,200 @@ +import { ArrowRightIcon } from "@heroicons/react/20/solid"; +import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; +import { + ForwardIcon, + SquaresPlusIcon, + UsersIcon, + WrenchScrewdriverIcon, +} from "@heroicons/react/24/solid"; +import { Link } from "@remix-run/react/dist/components"; +import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; +import { Header2 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { TextLink } from "~/components/primitives/TextLink"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server"; +import { requireUserId } from "~/services/session.server"; +import { Handle } from "~/utils/handle"; +import { + OrganizationParamsSchema, + PlansPath, + jobPath, + organizationTeamPath, +} from "~/utils/pathBuilder"; + +export async function loader({ params, request }: LoaderFunctionArgs) { + const userId = await requireUserId(request); + const { organizationSlug } = OrganizationParamsSchema.parse(params); + + const presenter = new OrgUsagePresenter(); + + const data = await presenter.call({ userId, slug: organizationSlug }); + + if (!data) { + throw new Response(null, { status: 404 }); + } + + return typedjson(data); +} + +const CustomTooltip = ({ active, payload, label }: TooltipProps) => { + if (active && payload) { + return ( +
+

{label}:

+

{payload[0].value}

+
+ ); + } + + return null; +}; + +export default function Page() { + const organization = useOrganization(); + const loaderData = useTypedLoaderData(); + + return ( + <> + + Increase concurrency + + } + className="mb-4" + > + Some of your Runs are being queued because your Run concurrency is limited to 50. + +
+
+
+ Total Runs this month + +
+
+

{loaderData.runsCount.toLocaleString()}

+ + {loaderData.runsCountLastMonth} runs last month + +
+
+
+
+ Total Jobs + +
+
+

{loaderData.totalJobs.toLocaleString()}

+ + {loaderData.totalJobs === loaderData.totalJobsLastMonth ? ( + <>No change since last month + ) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? ( + <>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month + ) : ( + <>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month + )} + +
+
+
+
+ Total Integrations + +
+
+

{loaderData.totalIntegrations.toLocaleString()}

+ + {loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? ( + <>No change since last month + ) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? ( + <> + +{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since last + month + + ) : ( + <> + -{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since last + month + + )} + +
+
+
+
+ Team members + +
+
+

{loaderData.totalMembers.toLocaleString()}

+ + Manage + + +
+
+
+
+
+ Job Runs per month + + + + `${value}`} + /> + } /> + + + +
+
+
+ Jobs + Runs +
+
+ {loaderData.jobs.map((job) => ( + +
+

{job.slug}

+

Project: {job.project.name}

+
+
{job._count.runs.toLocaleString()}
+ + ))} +
+
+
+ + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing.plans/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing.plans/route.tsx new file mode 100644 index 000000000..7c66b1f2f --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing.plans/route.tsx @@ -0,0 +1,23 @@ +import { PricingTiers, TierEnterprise, TierFree, TierPro } from "~/components/billing/PricingTiers"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; +import { Callout } from "~/components/primitives/Callout"; +import { Handle } from "~/utils/handle"; + +export const handle: Handle = { + breadcrumb: (match) => , +}; + +export default function Page() { + return ( +
+ + Some of your Runs are being queued because your Run concurrency is limited to 50. + + + + + + +
+ ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx index 763e99551..819496857 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx @@ -13,6 +13,8 @@ import { TextLink } from "~/components/primitives/TextLink"; import { useOrganization } from "~/hooks/useOrganizations"; import { OrganizationParamsSchema, + PlansPath, + UsagePath, jobPath, newProjectPath, organizationTeamPath, @@ -28,9 +30,13 @@ import { PageTitleRow, PageTitle, PageButtons, + PageDescription, + PageTabs, } from "~/components/primitives/PageHeader"; import { Handle } from "~/utils/handle"; import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; +import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; +import { Outlet } from "@remix-run/react"; export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request); @@ -66,133 +72,46 @@ export const handle: Handle = { export default function Page() { const organization = useOrganization(); - const loaderData = useTypedLoaderData(); return ( - + + + + Invoices + + + Manage card details + + + Upgrade + + + Current bill total: $0.00 + - -
-
-
- Total Runs this month - -
-
-

{loaderData.runsCount.toLocaleString()}

- - {loaderData.runsCountLastMonth} runs last month - -
-
-
-
- Total Jobs - -
-
-

{loaderData.totalJobs.toLocaleString()}

- - {loaderData.totalJobs === loaderData.totalJobsLastMonth ? ( - <>No change since last month - ) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? ( - <>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month - ) : ( - <>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month - )} - -
-
-
-
- Total Integrations - -
-
-

{loaderData.totalIntegrations.toLocaleString()}

- - {loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? ( - <>No change since last month - ) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? ( - <> - +{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since - last month - - ) : ( - <> - -{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since - last month - - )} - -
-
-
-
- Team members - -
-
-

{loaderData.totalMembers.toLocaleString()}

- - Manage - - -
-
-
-
-
- Job Runs per month - - - - `${value}`} - /> - } /> - - - -
-
-
- Jobs - Runs -
-
- {loaderData.jobs.map((job) => ( - -
-

{job.slug}

-

Project: {job.project.name}

-
-
{job._count.runs.toLocaleString()}
- - ))} -
-
+ +
+
diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 241eea6c5..555543d68 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -111,6 +111,14 @@ export function organizationBillingPath(organization: OrgForPath) { return `${organizationPath(organization)}/billing`; } +export function UsagePath(organization: OrgForPath) { + return `${organizationPath(organization)}/billing`; +} + +export function PlansPath(organization: OrgForPath) { + return `${organizationPath(organization)}/billing/plans`; +} + function organizationParam(organization: OrgForPath) { return organization.slug; }