72f3a4b128
* 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>
268 lines
5.5 KiB
TypeScript
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 };
|
|
}
|