Improved the onboarding experience

- Automatically refresh the jobs page when there are no jobs
 - Celebrate 🎊
This commit is contained in:
Eric Allam
2023-08-11 14:08:39 +01:00
parent c246578a81
commit 9dd7fb2748
10 changed files with 154 additions and 82 deletions
+3
View File
@@ -190,6 +190,9 @@ pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
pnpm install --frozen-lockfile
# Install playwrite browsers (ONE TIME ONLY)
npx playwright install
```
3. Set up the database
@@ -1,6 +1,9 @@
import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
import { Link, useSearchParams } from "@remix-run/react";
import { Link, useRevalidator } from "@remix-run/react";
import { useEffect, useState } from "react";
import { useEventSource } from "remix-utils";
import invariant from "tiny-invariant";
import gradientBackground from "~/assets/images/gradient-background.png";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
@@ -9,7 +12,7 @@ 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 { jobTestPath, projectStreamingPath } from "~/utils/pathBuilder";
import { Feedback } from "../Feedback";
import { CodeBlock } from "../code/CodeBlock";
import { InlineCode } from "../code/InlineCode";
@@ -33,20 +36,60 @@ import { TextLink } from "../primitives/TextLink";
import integrationButton from "./integration-button.png";
import selectEnvironment from "./select-environment.png";
import selectExample from "./select-example.png";
import gradientBackground from "~/assets/images/gradient-background.png";
const existingProjectValue = "use-existing-project";
const newProjectValue = "create-new-next-app";
type SelectionChoices = "use-existing-project" | "create-new-next-app";
export function HowToSetupYourProject() {
const project = useProject();
const devEnvironment = useDevEnvironment();
const appOrigin = useAppOrigin();
const [searchQuery, setSearchQuery] = useSearchParams();
const selectedValue = searchQuery.get("selectedValue");
const [selectedValue, setSelectedValue] = useState<SelectionChoices | null>(null);
invariant(devEnvironment, "devEnvironment is required");
const revalidator = useRevalidator();
const events = useEventSource(projectStreamingPath(project.id), {
event: "message",
});
useEffect(() => {
if (events !== null) {
// This uses https://www.npmjs.com/package/canvas-confetti
if ("confetti" in window && typeof window.confetti !== "undefined") {
var duration = 2.5 * 1000;
var end = Date.now() + duration;
(function frame() {
// launch a few confetti from the left edge
// @ts-ignore
window.confetti({
particleCount: 7,
angle: 60,
spread: 55,
origin: { x: 0 },
});
// and launch a few from the right edge
// @ts-ignore
window.confetti({
particleCount: 7,
angle: 120,
spread: 55,
origin: { x: 1 },
});
// keep going until we are out of time
if (Date.now() < end) {
requestAnimationFrame(frame);
}
})();
}
revalidator.revalidate();
}
// WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
}, [events]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div
className="-ml-4 -mt-4 h-full w-[calc(100%+32px)] bg-cover bg-no-repeat pt-20"
@@ -55,7 +98,7 @@ export function HowToSetupYourProject() {
<div className="mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in {selectedValue === newProjectValue ? "5" : "2"} minutes
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
</Header1>
<Feedback
button={
@@ -68,30 +111,30 @@ export function HowToSetupYourProject() {
</div>
<RadioGroup
className="mb-4 flex gap-x-2"
onValueChange={(value) => setSearchQuery({ selectedValue: value })}
onValueChange={(value) => setSelectedValue(value as SelectionChoices)}
>
<RadioGroupItem
label="Use an existing Next.js project"
description="Use Trigger.dev in an existing Next.js project in less than 2 mins."
value={existingProjectValue}
checked={selectedValue === existingProjectValue}
value="use-existing-project"
checked={selectedValue === "use-existing-project"}
variant="icon"
data-action={existingProjectValue}
data-action="use-existing-project"
icon={<NamedIcon className="h-12 w-12 text-green-600" name={"tree"} />}
/>
<RadioGroupItem
label="Create a new Next.js project"
description="This is the quickest way to try out Trigger.dev in a new Next.js project and takes 5 mins."
value={newProjectValue}
checked={selectedValue === newProjectValue}
value="create-new-next-app"
checked={selectedValue === "create-new-next-app"}
variant="icon"
data-action={newProjectValue}
data-action="create-new-next-app"
icon={<NamedIcon className="h-8 w-8 text-green-600" name={"sapling"} />}
/>
</RadioGroup>
{selectedValue && (
<>
{selectedValue === newProjectValue ? (
{selectedValue === "create-new-next-app" ? (
<>
<StepNumber stepNumber="1" title="Create a new Next.js project" />
<StepContentContainer>
@@ -159,20 +202,9 @@ export function HowToSetupYourProject() {
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="6" title="Check for Jobs" />
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example Job in the
list.
</Paragraph>
<Button
variant="primary/medium"
className="mt-4"
LeadingIcon="refresh"
onClick={() => window.location.reload()}
>
Refresh
</Button>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</>
) : (
@@ -198,20 +230,9 @@ export function HowToSetupYourProject() {
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="4" title="Check for Jobs" />
<StepNumber stepNumber="4" title="Wait for Jobs" displaySpinner />
<StepContentContainer>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example Job in the
list.
</Paragraph>
<Button
variant="primary/medium"
className="mt-4"
LeadingIcon="refresh"
onClick={() => window.location.reload()}
>
Refresh
</Button>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</>
)}
@@ -1,10 +1,12 @@
import { cn } from "~/utils/cn";
import { Header2 } from "./Headers";
import { Spinner } from "./Spinner";
export function StepNumber({
stepNumber,
active = false,
complete = false,
displaySpinner = false,
title,
className,
}: {
@@ -13,6 +15,7 @@ export function StepNumber({
complete?: boolean;
title?: React.ReactNode;
className?: string;
displaySpinner?: boolean;
}) {
return (
<div className={cn("mr-3", className)}>
@@ -28,7 +31,15 @@ export function StepNumber({
<span className="flex h-6 w-6 items-center justify-center rounded border border-slate-700 bg-slate-800 py-1 text-xs font-semibold text-dimmed shadow">
{complete ? "✓" : stepNumber}
</span>
<Header2>{title}</Header2>
{displaySpinner ? (
<div className="flex items-center gap-x-2">
<Header2>{title}</Header2>
<Spinner />
</div>
) : (
<Header2>{title}</Header2>
)}
</div>
)}
</div>
+2
View File
@@ -16,6 +16,7 @@ import { getUser } from "./services/session.server";
import { appEnvTitleTag } from "./utils";
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
import { useHighlight } from "./hooks/useHighlight";
import { ExternalScripts } from "remix-utils";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
@@ -104,6 +105,7 @@ function App() {
</HighlightErrorBoundary>
<Toast />
<ScrollRestoration />
<ExternalScripts />
<Scripts />
<LiveReload />
</body>
@@ -1,9 +1,8 @@
import { ArrowUpIcon } from "@heroicons/react/24/solid";
import { LoaderArgs } from "@remix-run/server-runtime";
import { GitHubLightIcon, OpenAILightIcon, ResendIcon } from "@trigger.dev/companyicons";
import { CalendarDaysIcon, ClockIcon, SlackIcon } from "lucide-react";
import { LoaderArgs, SerializeFrom } from "@remix-run/server-runtime";
import useWindowSize from "react-use/lib/useWindowSize";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { ExternalScriptsFunction } from "remix-utils";
import { HowToSetupYourProject } from "~/components/helpContent/HelpContentText";
import { JobsTable } from "~/components/jobs/JobsTable";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
@@ -59,6 +58,12 @@ export const loader = async ({ request, params }: LoaderArgs) => {
export const handle: Handle = {
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Jobs" />,
expandSidebar: true,
scripts: (match) => [
{
src: "https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js",
crossOrigin: "anonymous",
},
],
};
export default function Page() {
@@ -83,25 +88,6 @@ export default function Page() {
</PageInfoRow>
</PageHeader>
<PageBody>
{/* Todo: this confetti component needs to trigger when the example project is created, then never again. */}
{/* <Confetti
width={width}
height={height}
recycle={false}
numberOfPieces={1000}
colors={[
"#E7FF52",
"#41FF54",
"rgb(245 158 11)",
"rgb(22 163 74)",
"rgb(37 99 235)",
"rgb(67 56 202)",
"rgb(219 39 119)",
"rgb(225 29 72)",
"rgb(217 70 239)",
]}
/> */}
<Help>
{(open) => (
<div className={cn("grid gap-4", open ? "h-full grid-cols-2" : " h-full grid-cols-1")}>
@@ -0,0 +1,60 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { sse } from "~/utils/sse";
export async function loader({ request, params }: LoaderArgs) {
await requireUserId(request);
const { projectId } = z.object({ projectId: z.string() }).parse(params);
const project = await projectForUpdates(projectId);
if (!project) {
return new Response("Not found", { status: 404 });
}
let lastSignals = calculateChangeSignals(project);
return sse({
request,
run: async (send, stop) => {
const result = await projectForUpdates(projectId);
if (!result) {
return stop();
}
const newSignals = calculateChangeSignals(result);
if (lastSignals.jobCount !== newSignals.jobCount) {
send({ data: JSON.stringify(newSignals) });
}
lastSignals = newSignals;
},
});
}
function projectForUpdates(id: string) {
return prisma.project.findUnique({
where: {
id,
},
include: {
_count: {
select: { jobs: true },
},
},
});
}
function calculateChangeSignals(
project: NonNullable<Awaited<ReturnType<typeof projectForUpdates>>>
) {
const jobCount = project._count?.jobs ?? 0;
return {
jobCount,
};
}
+2
View File
@@ -1,6 +1,8 @@
import { ExternalScriptsFunction } from "remix-utils";
import { BreadcrumbItem } from "~/components/navigation/Breadcrumb";
export type Handle = {
breadcrumb?: BreadcrumbItem;
expandSidebar?: boolean;
scripts?: ExternalScriptsFunction;
};
+4
View File
@@ -131,6 +131,10 @@ export function projectEnvironmentsPath(organization: OrgForPath, project: Proje
return `${projectPath(organization, project)}/environments`;
}
export function projectStreamingPath(id: string) {
return `/resources/projects/${id}/jobs/stream`;
}
export function projectEnvironmentsStreamingPath(
organization: OrgForPath,
project: ProjectForPath
-1
View File
@@ -91,7 +91,6 @@
"prism-react-renderer": "^1.3.5",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.0",
"react-hotkeys-hook": "^3.4.7",
-16
View File
@@ -160,7 +160,6 @@ importers:
prismjs: ^1.29.0
prop-types: ^15.8.1
react: ^18.2.0
react-confetti: ^6.1.0
react-dom: ^18.2.0
react-hot-toast: ^2.4.0
react-hotkeys-hook: ^3.4.7
@@ -254,7 +253,6 @@ importers:
prism-react-renderer: 1.3.5_react@18.2.0
prismjs: 1.29.0
react: 18.2.0
react-confetti: 6.1.0_react@18.2.0
react-dom: 18.2.0_react@18.2.0
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
react-hotkeys-hook: 3.4.7_biqbaboplfbrettd7655fr4n2y
@@ -22320,16 +22318,6 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: true
/react-confetti/6.1.0_react@18.2.0:
resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==}
engines: {node: '>=10.18'}
peerDependencies:
react: ^16.3.0 || ^17.0.1 || ^18.0.0
dependencies:
react: 18.2.0
tween-functions: 1.2.0
dev: false
/react-docgen-typescript/2.2.2_typescript@4.9.4:
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
peerDependencies:
@@ -25110,10 +25098,6 @@ packages:
turbo-windows-arm64: 1.10.3
dev: true
/tween-functions/1.2.0:
resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==}
dev: false
/type-check/0.3.2:
resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
engines: {node: '>= 0.8.0'}