Implement the cloud private beta invite form
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled

This commit is contained in:
Eric Allam
2023-07-05 17:27:59 +01:00
parent fc2d337a39
commit 20ed4e22ef
7 changed files with 180 additions and 18 deletions
@@ -1,8 +1,17 @@
import { Link } from "@remix-run/react";
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
import { jobTestPath } from "~/utils/pathBuilder";
import { InlineCode } from "../code/InlineCode";
import { HelpPanelProps } from "../integrations/ApiKeyHelp";
import { HelpInstall } from "../integrations/HelpInstall";
import { HelpSamples } from "../integrations/HelpSamples";
import { Button, LinkButton } from "../primitives/Buttons";
import { Callout, variantClasses } from "../primitives/Callout";
import {
@@ -15,17 +24,6 @@ import { ClipboardField } from "../primitives/ClipboardField";
import integrationButton from "./integration-button.png";
import selectEnvironment from "./select-environment.png";
import selectExample from "./select-example.png";
import { InlineCode } from "../code/InlineCode";
import { ApiKeyHelp, HelpPanelProps } from "../integrations/ApiKeyHelp";
import { CodeBlock } from "../code/CodeBlock";
import { HelpInstall } from "../integrations/HelpInstall";
import { HelpSamples } from "../integrations/HelpSamples";
import { jobTestPath } from "~/utils/pathBuilder";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useJob } from "~/hooks/useJob";
import { Link } from "@remix-run/react";
import { OrderedList, OrderedListItem } from "../primitives/Lists";
export function HowToSetupYourProject() {
const devEnvironment = useDevEnvironment();
@@ -1,6 +1,6 @@
import * as React from "react";
import { cn } from "~/utils/cn";
import type { IconNames } from "./NamedIcon";
import type { IconNamesOrString } from "./NamedIcon";
import { NamedIcon } from "./NamedIcon";
const variants = {
@@ -44,7 +44,7 @@ const variants = {
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
variant?: keyof typeof variants;
icon?: IconNames;
icon?: IconNamesOrString;
shortcut?: string;
fullWidth?: boolean;
};
@@ -228,6 +228,7 @@ const icons = {
};
export type IconNames = keyof typeof icons;
export type IconNamesOrString = IconNames | (string & {});
export const iconNames = Object.keys(icons) as IconNames[];
export function NamedIcon({
@@ -1,6 +1,6 @@
import { Link } from "@remix-run/react";
import { cn } from "~/utils/cn";
import { IconNames, NamedIcon } from "./NamedIcon";
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
const paragraphVariants = {
base: {
@@ -95,13 +95,13 @@ type TextLinkProps = {
href?: string;
to?: string;
className?: string;
trailingIcon?: IconNames;
trailingIcon?: IconNamesOrString;
trailingIconClassName?: string;
children: React.ReactNode;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
const classes =
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center";
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group";
export function TextLink({
href,
+1 -1
View File
@@ -12,7 +12,7 @@ export function featuresForRequest(request: Request): TriggerFeatures {
const isManagedCloud =
url.host === "cloud.trigger.dev" ||
url.host === "test-cloud.trigger.dev" ||
process.env.NODE_ENV === "development";
process.env.CLOUD_ENV === "development";
return {
isManagedCloud,
+19
View File
@@ -128,3 +128,22 @@ export function updateUser({
data: { name, email, marketingEmails, confirmedBasicDetails: true },
});
}
export async function grantUserCloudAccess({
id,
inviteCode,
}: {
id: string;
inviteCode: string;
}) {
return prisma.user.update({
where: { id },
data: {
invitationCode: {
connect: {
code: inviteCode,
},
},
},
});
}
+145 -1
View File
@@ -1,15 +1,159 @@
import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { Form, useActionData } from "@remix-run/react";
import { ActionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import {
AppContainer,
MainCenteredContainer,
} from "~/components/layout/AppLayout";
import { NavBar } from "~/components/navigation/NavBar";
import { Button } from "~/components/primitives/Buttons";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { FormTitle } from "~/components/primitives/FormTitle";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { TextLink } from "~/components/primitives/Paragraph";
import { prisma } from "~/db.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { grantUserCloudAccess } from "~/models/user.server";
import { requireUserId } from "~/services/session.server";
import { organizationsPath } from "~/utils/pathBuilder";
function createSchema(
constraints: {
isValidCode?: (code: string) => Promise<boolean>;
} = {}
) {
return z.object({
code: z
.string()
.min(1, "Invite code missing")
.superRefine((code, ctx) => {
if (constraints.isValidCode === undefined) {
//client-side validation skips this
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: conform.VALIDATION_UNDEFINED,
});
} else {
// Tell zod this is an async validation by returning the promise
return constraints.isValidCode(code).then((isValid) => {
if (isValid) {
return;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid invitation code",
});
});
}
}),
});
}
export async function action({ request }: ActionArgs) {
const userId = await requireUserId(request);
const formData = await request.formData();
const formSchema = createSchema({
isValidCode: async (code) => {
const invitationCode = await prisma.invitationCode.findUnique({
where: {
code,
},
});
return invitationCode !== undefined && invitationCode !== null;
},
});
const submission = await parse(formData, { schema: formSchema, async: true });
if (!submission.value) {
return json(submission);
}
try {
await grantUserCloudAccess({
id: userId,
inviteCode: submission.value.code,
});
return redirectWithSuccessMessage(
organizationsPath(),
request,
"🚀 Welcome to the Trigger.dev Cloud private beta"
);
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
}
}
export default function Page() {
const lastSubmission = useActionData();
const [form, { code }] = useForm({
id: "invitation-code",
lastSubmission,
shouldRevalidate: "onSubmit",
onValidate({ formData }) {
return parse(formData, { schema: createSchema() });
},
});
return (
<AppContainer showBackgroundGradient={true}>
<NavBar />
<MainCenteredContainer>
<div>Enter your invitation code to get access to the cloud.</div>
<FormTitle
LeadingIcon="qr-code"
title="Trigger.dev Cloud Beta"
description={
<>
Enter your code to get access. If you don't have one you can
always{" "}
<TextLink
target="_blank"
href="https://trigger.dev/docs/documentation/guides/self-hosting"
trailingIcon="external-link"
trailingIconClassName="h-3 w-3 text-indigo-500 transition group-hover:text-indigo-400"
>
self-host
</TextLink>{" "}
Trigger.dev
</>
}
/>
<Form method="post" {...form.props}>
<Fieldset>
<InputGroup>
<Input
{...conform.input(code, { type: "text" })}
placeholder="Your super secret invite code"
icon="qr-code"
autoFocus={Boolean(code.initialError)}
spellCheck={false}
/>
<FormError id={code.errorId}>{code.error}</FormError>
</InputGroup>
<FormButtons
confirmButton={
<Button
type="submit"
variant={"primary/small"}
TrailingIcon={"arrow-right"}
>
Get access
</Button>
}
/>
</Fieldset>
</Form>
</MainCenteredContainer>
</AppContainer>
);