Improved the onboarding experience
- Automatically refresh the jobs page when there are no jobs
- Celebrate 🎊
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+8
-22
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ExternalScriptsFunction } from "remix-utils";
|
||||
import { BreadcrumbItem } from "~/components/navigation/Breadcrumb";
|
||||
|
||||
export type Handle = {
|
||||
breadcrumb?: BreadcrumbItem;
|
||||
expandSidebar?: boolean;
|
||||
scripts?: ExternalScriptsFunction;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
-16
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user