Files
Matt Aitken 72f3a4b128 Usage and billing (v3) (#1201)
* v3 subscription endpoints

* Use pnpm linked billing package during development

* Moved v2 billing components into a subfolder

* Select plan using the real data

* Improved v3 plan display

* Use new api response that doesn’t require a Stripe call

* Free flow is working

* Added GitHub modal and verified badge

* Deleted old request v3 access component/route

* Allow setting classes on the Tooltip button

* Paid plans working

* Redirect from select plan if you’ve got v3 enabled

* Loading state improvements

* New admin API endpoint to set concurrency across multiple environments

* When projects are created, conditionally create staging based on the plan

* New billing page working with side menu and stripe portal

* Layout, formatting and some plan state improvements/fixes

* Don’t show the period if you’re on the free plan

* Temporary upgrade callout

* Refactored the platform code so it’s easier to call and doesn’t require a isManagedCloud check

* Send taskIdentifier to OpenMeter

* Side menu

* Upgrade prompts

* More improvements to the app-wide usage indicators

* Early work on usage graphs

* Added the usage bar for v3

* Moved code to presenter and now using defer

* Added the tasks table to usage

* Improved the v3 usage bar if theres’ no usage on a paid plan

* If no run data, still render a graph

* Usage page errors when defered loading fails

* Don’t show the public API key for v3, they’re not used and probably never will be

* Improved the upgrade callout and API keys page layout

* Only show the “reveal all” toggle if you have environment variables in the table

* Replaced Upgrade callout with a more generic InfoPanel component

* better panel width

* Show conditional upgrade prompts based on plan and number of schedules used

* Removed duplicate class

* Wider blank state panels for the scheduled page

* Wider info panel for the env var page

* Blank state now using the info panel

* Platform alerts prompt now using the InfoPanel

* Deploy blank state uses InfoPanel

* Github verified badge padding adjustment

* Improved the layout of the page, some style tweaks, organized imports

* Better default tooltip style

* could be undefined fix

* text fix + style updates

* Changed the billing icon in the side menu

* Billing page layout and style improvements

* plan tooltips don’t use dark variant

* Don’t highlight the plan on the billing page

* Tooltip underlines stand out more

* Fixed padding in the PageTitle

* Tooltips use the correct cursor

* Improved the plan banner on the billing page

* Fixed Header1 inconsistent font weight

* Fixed issue where input field focus states were being clipped

* Fixed large button not having large text size

* Added a link to the Get in touch copy and improved the connect to GitHub modal

* Select plan page uses the MainCenteredContainer

* Better logging from the Loops endpoint because this error finally got hit

* Move the ingestion of compute to the platform

* Reporting usage of invocations moved to the platform

* Get the entitlement before triggering a non-dev task

* Contact us enterprise plan button opens the feedback form

* Removed Github discussions link from the Feedback panel

* Swapped billing icon for credit card

* Show a Unlock staging panel on the env var page

* Updated staging environment colour

* Show a prompt to upgrade to get staging in the new env var modal

* Improved the edit env var modal

* Implement ability to disable org concurrency

* Use common logic for the plans

* Use the billing server to get the schedule limits

* Some schedules page fixes

* More convenient way of getting a limit

* Use the new schedule limit

* Team member limiting

* Made the limit visible on the team page

* Limit alerts

* Added an index for TaskRun.scheduleId

* Remove console.log on schedules page

* Added durations to the run table

* Tabular numbers

* Improved the usage page formatting

* Only admins see the compute column on the run table

* Include the base cost on the usage stats

* Moved the status to the sidebar

* Optional table header tooltip

* Allow InfoIconTooltips to have customizable content styles

* Added a tooltip to the duration header, changed no test to a dash

* Removed all references to signing up to v3 from the docs

* Switched @trigger.dev/billing to @trigger.dev/platform

* Passing up the variant for the InfoIconTooltip

* table tooltip max-width fixed

* Switch to the published @trigger.dev/platform 1.0.11

* v2 usage page title changed to include “v2"

* code theme has a transparent background so it works on any background

* duration columns now grouped together nicely at wide screen size

* Last duration column fills the width properly

* Fix for the per run price being in cents not dollars

* Show the total cost with 8 decimal places

* Show 8 decimal places in the usage graph tooltip

* Moved the UpgradePrompt to the v3 folder

* Prepare to use Shadcns chart helpers

* Much nicer chart

* Small tweaks to the graph

* Fix run table col spans for empty/loading messages

* We don’t need isManagedCloud in createProject

* Hide v3 usage/billing pages if there aren’t v3 projects in your org

* Removed unused tooltipStyle

* Usage bar now says “Included usage” instead of “Tier limit” if you’re paying

* Get the plan/usage data in parallel

* The usage page now has a month dropdown and all data is for that calendar month

* Ensure the passed date is the 1st of the month

* Use the machine presets from the platform package

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
Co-authored-by: Eric Allam <eallam@icloud.com>
2024-07-08 14:42:19 +01:00

268 lines
5.5 KiB
TypeScript

import { prisma } from "~/db.server";
import { createEnvironment } from "./organization.server";
export async function getTeamMembersAndInvites({
userId,
organizationId,
}: {
userId: string;
organizationId: string;
}) {
const org = await prisma.organization.findFirst({
where: { id: organizationId, members: { some: { userId } } },
select: {
members: {
select: {
id: true,
role: true,
user: {
select: {
id: true,
name: true,
email: true,
avatarUrl: true,
},
},
},
},
invites: {
select: {
id: true,
email: true,
updatedAt: true,
inviter: {
select: {
id: true,
name: true,
email: true,
avatarUrl: true,
},
},
},
},
},
});
if (!org) {
return null;
}
return { members: org.members, invites: org.invites };
}
export async function removeTeamMember({
userId,
slug,
memberId,
}: {
userId: string;
slug: string;
memberId: string;
}) {
const org = await prisma.organization.findFirst({
where: { slug, members: { some: { userId } } },
});
if (!org) {
throw new Error("User does not have access to this organization");
}
return prisma.orgMember.delete({
where: {
id: memberId,
},
include: {
organization: true,
user: true,
},
});
}
export async function inviteMembers({
slug,
emails,
userId,
}: {
slug: string;
emails: string[];
userId: string;
}) {
const org = await prisma.organization.findFirst({
where: { slug, members: { some: { userId } } },
});
if (!org) {
throw new Error("User does not have access to this organization");
}
const created = await prisma.orgMemberInvite.createMany({
data: emails.map((email) => ({
email,
organizationId: org.id,
inviterId: userId,
role: "MEMBER",
})),
skipDuplicates: true,
});
return await prisma.orgMemberInvite.findMany({
where: {
organizationId: org.id,
inviterId: userId,
email: {
in: emails,
},
},
include: {
organization: true,
inviter: true,
},
});
}
export async function getInviteFromToken({ token }: { token: string }) {
return await prisma.orgMemberInvite.findFirst({
where: {
token,
},
include: {
organization: true,
inviter: true,
},
});
}
export async function getUsersInvites({ email }: { email: string }) {
return await prisma.orgMemberInvite.findMany({
where: {
email,
organization: {
deletedAt: null,
},
},
include: {
organization: true,
inviter: true,
},
});
}
export async function acceptInvite({ userId, inviteId }: { userId: string; inviteId: string }) {
return await prisma.$transaction(async (tx) => {
// 1. Delete the invite and get the invite details
const invite = await tx.orgMemberInvite.delete({
where: {
id: inviteId,
},
include: {
organization: {
include: {
projects: true,
},
},
},
});
// 2. Join the organization
const member = await tx.orgMember.create({
data: {
organizationId: invite.organizationId,
userId,
role: invite.role,
},
});
// 3. Create an environment for each project
for (const project of invite.organization.projects) {
await createEnvironment(invite.organization, project, "DEVELOPMENT", member, tx);
}
// 4. Check for other invites
const remainingInvites = await tx.orgMemberInvite.findMany({
where: {
email: invite.email,
},
});
return { remainingInvites, organization: invite.organization };
});
}
export async function declineInvite({ userId, inviteId }: { userId: string; inviteId: string }) {
return await prisma.$transaction(async (tx) => {
//1. delete invite
const declinedInvite = await prisma.orgMemberInvite.delete({
where: {
id: inviteId,
},
include: {
organization: true,
},
});
//2. get email
const user = await prisma.user.findUnique({
where: { id: userId },
select: { email: true },
});
//3. check for other invites
const remainingInvites = await prisma.orgMemberInvite.findMany({
where: {
email: user!.email,
},
});
return { remainingInvites, organization: declinedInvite.organization };
});
}
export async function resendInvite({ inviteId }: { inviteId: string }) {
return await prisma.orgMemberInvite.update({
where: {
id: inviteId,
},
data: {
updatedAt: new Date(),
},
include: {
inviter: true,
organization: true,
},
});
}
export async function revokeInvite({
userId,
slug,
inviteId,
}: {
userId: string;
slug: string;
inviteId: string;
}) {
const org = await prisma.organization.findFirst({
where: { slug, members: { some: { userId } } },
});
if (!org) {
throw new Error("User does not have access to this organization");
}
const invite = await prisma.orgMemberInvite.delete({
where: {
id: inviteId,
organizationId: org.id,
},
select: {
email: true,
organization: true,
},
});
if (!invite) {
throw new Error("Invite not found");
}
return { email: invite.email, organization: invite.organization };
}