From 9dd7fb2748df3d84a0cce51437feff7163c972cc Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 11 Aug 2023 14:08:39 +0100 Subject: [PATCH] Improved the onboarding experience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Automatically refresh the jobs page when there are no jobs - Celebrate 🎊 --- CONTRIBUTING.md | 3 + .../helpContent/HelpContentText.tsx | 105 +++++++++++------- .../app/components/primitives/StepNumber.tsx | 13 ++- apps/webapp/app/root.tsx | 2 + .../route.tsx | 30 ++--- ...sources.projects.$projectId.jobs.stream.ts | 60 ++++++++++ apps/webapp/app/utils/handle.ts | 2 + apps/webapp/app/utils/pathBuilder.ts | 4 + apps/webapp/package.json | 1 - pnpm-lock.yaml | 16 --- 10 files changed, 154 insertions(+), 82 deletions(-) create mode 100644 apps/webapp/app/routes/resources.projects.$projectId.jobs.stream.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a12d6b80c0..7248490030 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/apps/webapp/app/components/helpContent/HelpContentText.tsx b/apps/webapp/app/components/helpContent/HelpContentText.tsx index 8519ff422f..40a9416fee 100644 --- a/apps/webapp/app/components/helpContent/HelpContentText.tsx +++ b/apps/webapp/app/components/helpContent/HelpContentText.tsx @@ -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(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 (
- Get setup in {selectedValue === newProjectValue ? "5" : "2"} minutes + Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes setSearchQuery({ selectedValue: value })} + onValueChange={(value) => setSelectedValue(value as SelectionChoices)} > } /> } /> {selectedValue && ( <> - {selectedValue === newProjectValue ? ( + {selectedValue === "create-new-next-app" ? ( <> @@ -159,20 +202,9 @@ export function HowToSetupYourProject() { - + - - Once you've run the CLI command, click Refresh to view your example Job in the - list. - - + This page will automatically refresh. ) : ( @@ -198,20 +230,9 @@ export function HowToSetupYourProject() { - + - - Once you've run the CLI command, click Refresh to view your example Job in the - list. - - + This page will automatically refresh. )} diff --git a/apps/webapp/app/components/primitives/StepNumber.tsx b/apps/webapp/app/components/primitives/StepNumber.tsx index ae7759c385..1ae524bff6 100644 --- a/apps/webapp/app/components/primitives/StepNumber.tsx +++ b/apps/webapp/app/components/primitives/StepNumber.tsx @@ -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 (
@@ -28,7 +31,15 @@ export function StepNumber({ {complete ? "✓" : stepNumber} - {title} + + {displaySpinner ? ( +
+ {title} + +
+ ) : ( + {title} + )}
)}
diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 90b021f873..589878a208 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -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() { + diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx index 384a272754..8c487f6911 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx @@ -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) => , 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() { - {/* Todo: this confetti component needs to trigger when the example project is created, then never again. */} - {/* */} - {(open) => (
diff --git a/apps/webapp/app/routes/resources.projects.$projectId.jobs.stream.ts b/apps/webapp/app/routes/resources.projects.$projectId.jobs.stream.ts new file mode 100644 index 0000000000..574f77bd07 --- /dev/null +++ b/apps/webapp/app/routes/resources.projects.$projectId.jobs.stream.ts @@ -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>> +) { + const jobCount = project._count?.jobs ?? 0; + + return { + jobCount, + }; +} diff --git a/apps/webapp/app/utils/handle.ts b/apps/webapp/app/utils/handle.ts index 60ed8d431f..3613b5c671 100644 --- a/apps/webapp/app/utils/handle.ts +++ b/apps/webapp/app/utils/handle.ts @@ -1,6 +1,8 @@ +import { ExternalScriptsFunction } from "remix-utils"; import { BreadcrumbItem } from "~/components/navigation/Breadcrumb"; export type Handle = { breadcrumb?: BreadcrumbItem; expandSidebar?: boolean; + scripts?: ExternalScriptsFunction; }; diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index e3affdca60..95a5842e8a 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -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 diff --git a/apps/webapp/package.json b/apps/webapp/package.json index d07555e0ff..7238115a02 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 676cbeee65..3443c3a67a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'}