Improvements to the usage page

This commit is contained in:
Matt Aitken
2023-12-05 11:55:05 +00:00
parent 907dbee71f
commit e113d6e41f
5 changed files with 110 additions and 169 deletions
@@ -35,7 +35,6 @@ export function ConcurrentRunsChart({
}) {
return (
<div className="relative">
<Header3 className="mb-4">Concurrent Runs</Header3>
{!hasConcurrencyData && (
<Paragraph className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
No concurrent Runs to show
@@ -52,20 +51,33 @@ export function ConcurrentRunsChart({
}}
className="-ml-8"
>
<XAxis stroke="#94A3B8" fontSize={12} tickLine={false} axisLine={false} dataKey="name" />
<XAxis
stroke="#94A3B8"
fontSize={12}
tickLine={false}
axisLine={false}
dataKey="name"
label={"Last 30 days"}
/>
<YAxis stroke="#94A3B8" fontSize={12} tickLine={false} axisLine={false} />
<Tooltip cursor={{ fill: "rgba(255,255,255,0.05)" }} contentStyle={tooltipStyle} />
{concurrentRunsLimit && (
<ReferenceLine
y={concurrentRunsLimit}
label={<ReferenceLineLabel y={concurrentRunsLimit} />}
stroke="#F43F5E"
color="#fff"
color="#F43F5E"
strokeWidth={1}
strokeDasharray={5}
ifOverflow="extendDomain"
className="text-xs"
/>
>
<Label
value="Concurrent Runs limit"
offset={5}
position="insideBottomLeft"
fill="#F43F5E"
/>
</ReferenceLine>
)}
<Line
dataKey="maxConcurrentRuns"
@@ -79,13 +91,3 @@ export function ConcurrentRunsChart({
</div>
);
}
function ReferenceLineLabel({ y }: { y: number }) {
return (
<text x={0} y={y} fill={"#F43F5E"}>
<tspan x={"8em"} dy={"0.3em"}>
Concurrency limit
</tspan>
</text>
);
}
@@ -28,11 +28,6 @@ export class OrgUsagePresenter {
return;
}
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const startOfLastMonth = new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1); // this works for January as well
const endOfMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1);
endOfMonth.setDate(endOfMonth.getDate() - 1);
// Get count of runs since the start of the current month
const runsCount = await this.#prismaClient.jobRun.count({
where: {
@@ -44,18 +39,6 @@ export class OrgUsagePresenter {
},
});
// Get the count of runs for last month
const runsCountLastMonth = await this.#prismaClient.jobRun.count({
where: {
organizationId: organization.id,
createdAt: {
gte: startOfLastMonth,
lt: startOfMonth,
},
internal: false,
},
});
// Get the count of the runs for the last 6 months, by month. So for example we want the data shape to be:
// [
// { month: "2021-01", count: 10 },
@@ -82,45 +65,6 @@ export class OrgUsagePresenter {
const monthlyRunsDataDisplay = fillInMissingRunMonthlyData(monthlyRunsData, 6);
const totalJobs = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
internal: false,
},
});
const totalJobsLastMonth = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
deletedAt: null,
internal: false,
},
});
const totalIntegrations = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
},
});
const totalIntegrationsLastMonth = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
},
});
const totalMembers = await this.#prismaClient.orgMember.count({
where: {
organizationId: organization.id,
},
});
// Max concurrency each day over past 30 days
const concurrencyChartRawData = await this.#prismaClient.$queryRaw<
{ day: Date; max_concurrent_runs: BigInt }[]
@@ -173,6 +117,8 @@ export class OrgUsagePresenter {
concurrencyChartRawData
);
const endOfMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1);
endOfMonth.setDate(endOfMonth.getDate() - 1);
const projectedRunsCount = Math.round(
runsCount / (new Date().getDate() / endOfMonth.getDate())
);
@@ -202,16 +148,10 @@ export class OrgUsagePresenter {
id: organization.id,
runsCount,
projectedRunsCount,
runsCountLastMonth,
monthlyRunsData: monthlyRunsDataDisplay,
hasMonthlyRunData,
concurrencyData: concurrencyChartRawDataFilledIn,
hasConcurrencyData,
totalJobs,
totalJobsLastMonth,
totalIntegrations,
totalIntegrationsLastMonth,
totalMembers,
runCostEstimation,
projectedRunCostEstimation,
};
@@ -260,15 +200,21 @@ function fillInMissingConcurrencyDays(
for (let i = 0; i < days; i++) {
const date = new Date(startDate);
date.setDate(date.getDate() + i);
let formattedDate = `${date.getDate()}`;
if (date.getDate() === 1) {
formattedDate = dateFormatter.format(date);
}
const foundData = data.find((d) => d.day.toISOString() === date.toISOString());
if (!foundData) {
outputData.push({
name: dateFormatter.format(date),
name: formattedDate,
maxConcurrentRuns: 0,
});
} else {
outputData.push({
name: dateFormatter.format(date),
name: formattedDate,
maxConcurrentRuns: Number(foundData.max_concurrent_runs),
});
}
@@ -302,11 +248,3 @@ function getMonthsBetween(startMonth: string, endMonth: string): string[] {
return result;
}
function getLastSecondOfMonth(endMonth: string) {
const [year, month] = endMonth.split("-").map(Number);
const nextMonthFirstDay = new Date(year, month, 1);
nextMonthFirstDay.setDate(0);
nextMonthFirstDay.setHours(23, 59, 59);
return nextMonthFirstDay;
}
@@ -188,78 +188,6 @@ export default function Page() {
</div>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header3>Total Runs this month</Header3>
<NamedIcon className="h-6 w-6 text-dimmed/50" name={"runs"} />
</div>
<div>
<p className="text-3xl font-medium">{loaderData.runsCount.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.runsCountLastMonth} Runs last month
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header3>Total Jobs</Header3>
<WrenchScrewdriverIcon className="h-6 w-6 text-dimmed/50" />
</div>
<div>
<p className="text-3xl font-medium">{loaderData.totalJobs.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{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</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header3>Total Integrations</Header3>
<SquaresPlusIcon className="h-6 w-6 text-dimmed/50" />
</div>
<div>
<p className="text-3xl font-medium">{loaderData.totalIntegrations.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{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
</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header3>Team members</Header3>
<UsersIcon className="h-6 w-6 text-dimmed/50" />
</div>
<div>
<p className="text-3xl font-medium">{loaderData.totalMembers.toLocaleString()}</p>
<TextLink
to={organizationTeamPath(organization)}
className="group text-sm text-dimmed hover:text-bright"
>
Manage
<ArrowRightIcon className="-mb-0.5 ml-0.5 h-4 w-4 text-dimmed transition group-hover:translate-x-1 group-hover:text-bright" />
</TextLink>
</div>
</div>
</div>
</div>
);
}
@@ -29,12 +29,6 @@ import { OrganizationParamsSchema, plansPath, usagePath } from "~/utils/pathBuil
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { ActiveSubscription } from "@trigger.dev/billing";
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
return typedjson({});
}
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={match.pathname} title="Usage & Billing" />,
};
@@ -74,16 +68,16 @@ export default function Page() {
<PageButtons>
{isManagedCloud && (
<>
{/* {stripePortalLink && (
{currentPlan?.subscription?.isPaying && (
<>
<LinkButton to={stripePortalLink} variant="secondary/small">
{/* <LinkButton to={stripePortalLink} variant="secondary/small">
Invoices
</LinkButton>
<LinkButton to={stripePortalLink} variant="secondary/small">
Manage card details
</LinkButton>
</LinkButton> */}
</>
)} */}
)}
<LinkButton
to={plansPath(organization)}
variant="primary/small"
@@ -0,0 +1,79 @@
import { parse } from "@conform-to/zod";
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { SetPlanBodySchema } from "@trigger.dev/billing";
import { redirect } from "remix-typedjson";
import { prisma } from "~/db.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { BillingService } from "~/services/billing.server";
import { logger } from "~/services/logger.server";
import { requireUser } from "~/services/session.server";
import {
OrganizationParamsSchema,
organizationBillingPath,
subscribedPath,
} from "~/utils/pathBuilder";
export async function action({ request, params }: ActionFunctionArgs) {
const user = await requireUser(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const formData = await request.formData();
const submission = parse(formData, { schema: SetPlanBodySchema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
const org = await prisma.organization.findUnique({
select: {
id: true,
},
where: {
slug: organizationSlug,
members: {
some: {
userId: user.id,
},
},
},
});
if (!org) {
submission.error.message = "Invalid organization";
return json(submission);
}
const billingPresenter = new BillingService(true);
const result = await billingPresenter.setPlan(org.id, submission.value);
if (result === undefined) {
submission.error.message = "No billing client";
return json(submission);
}
if (!result.success) {
submission.error.message = result.error;
return json(submission);
}
switch (result.action) {
case "create_subscription_flow_start": {
return redirect(result.checkoutUrl);
}
case "canceled_subscription": {
return redirectWithSuccessMessage(
organizationBillingPath({ slug: organizationSlug }),
request,
"Your subscription has been canceled."
);
}
case "updated_subscription": {
return redirect(subscribedPath({ slug: organizationSlug }), request);
}
}
} catch (e) {
logger.error("Error setting plan", { error: e });
submission.error.message = e instanceof Error ? e.message : JSON.stringify(e);
return json(submission);
}
}