Tidied up more old components

This commit is contained in:
Matt Aitken
2023-05-10 08:44:41 +01:00
parent 6f35b2e17d
commit 2765fe12cd
33 changed files with 324 additions and 1477 deletions
@@ -7,7 +7,7 @@ import {
} from "@heroicons/react/24/outline";
import classNames from "classnames";
import { useCallback, useState } from "react";
import { CopyText } from "./CopyText";
import { CopyText } from "./primitives/CopyText";
import { TertiaryButton } from "./primitives/Buttons";
import { Body } from "./primitives/text/Body";
@@ -8,7 +8,6 @@ import "prismjs/components/prism-bash";
import "prismjs/plugins/line-numbers/prism-line-numbers";
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
import { CopyTextButton } from "../CopyTextButton";
import classNames from "classnames";
Prism.manual = true;
@@ -3,7 +3,7 @@ import classNames from "classnames";
import { useTypedFetcher } from "remix-typedjson";
import type { action } from "~/routes/resources/connection/oauth2";
import type { ExternalApi } from "~/services/externalApis/types";
import { NamedIcon } from "../Icon";
import { NamedIcon } from "../primitives/NamedIcon";
import { PrimaryButton } from "../primitives/Buttons";
import {
Sheet,
@@ -26,11 +26,11 @@ const btnVariants = {
const iconVariants = {
size: {
// ExtraSmall: "h-3",
// extraSmall: "h-3",
small: "h-4",
medium: "h-4",
large: "h-5",
// ExtraLarge: "h-6",
// extraLarge: "h-6",
},
theme: {
primary: "text-slate-900",
@@ -14,7 +14,7 @@ export function CopyText({
onCopied,
}: CopyTextProps) {
const onClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
navigator.clipboard.writeText(value);
@@ -26,8 +26,8 @@ export function CopyText({
);
return (
<div onClick={onClick} className={`${className}`}>
<button onClick={onClick} className={`${className}`}>
{children}
</div>
</button>
);
}
+125 -64
View File
@@ -1,66 +1,127 @@
import { Dialog as HeadlessDialog, Transition } from "@headlessui/react";
import { forwardRef, Fragment } from "react";
"use client";
type DialogProps = Parameters<typeof HeadlessDialog>[0] & {
children: React.ReactNode;
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "~/utils/cn";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in sm:max-w-lg sm:rounded-lg sm:zoom-in-90",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
//todo change to use ShadCn
function Dialog({ onClose, children, ...props }: DialogProps) {
return (
<Transition {...props}>
<HeadlessDialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/70" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
{children}
</Transition.Child>
</div>
</div>
</HeadlessDialog>
</Transition>
);
}
type PanelProps = Parameters<typeof HeadlessDialog.Panel>[0];
const Panel = forwardRef((props: PanelProps, ref) => (
<HeadlessDialog.Panel
className="w-full max-w-xl transform overflow-hidden rounded-md bg-slate-800 p-10 text-left align-middle text-slate-200 shadow-md transition-all"
{...props}
ref={ref}
/>
));
Panel.displayName = "Dialog.Panel";
type TitleProps = Parameters<typeof HeadlessDialog.Title>[0];
const Title = forwardRef((props: TitleProps, ref) => (
<HeadlessDialog.Title
as="h3"
className="text-2xl leading-6 text-slate-200"
{...props}
ref={ref}
/>
));
Title.displayName = "Dialog.Title";
export const StyledDialog = { Dialog, Panel, Title };
@@ -1,3 +1,5 @@
import { cn } from "~/utils/cn";
export function Spinner({ className }: { className?: string }) {
return (
<svg
@@ -6,7 +8,7 @@ export function Spinner({ className }: { className?: string }) {
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`animate-spin ${className}`}
className={cn("animate-spin", className)}
>
<rect
x="2"
+48 -125
View File
@@ -1,131 +1,54 @@
import { Tab as HeadlessTab } from "@headlessui/react";
import classNames from "classnames";
import classnames from "classnames";
"use client";
type HeadlessTabProps = Parameters<typeof HeadlessTab>[0];
type HeadlessTabListProps = Parameters<typeof HeadlessTab.List>[0];
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "~/utils/cn";
export function ClassicList({ children, ...props }: HeadlessTabListProps) {
return (
<HeadlessTab.List className={"-mb-px flex bg-slate-50"} {...props}>
{children}
</HeadlessTab.List>
);
}
const Tabs = TabsPrimitive.Root;
export function Classic({ children, ...props }: HeadlessTabProps) {
return (
<HeadlessTab
className={({ selected }: { selected: boolean }) =>
classnames(
selected
? "border-t border-slate-200 bg-white text-slate-600"
: "border-b border-t border-slate-200 bg-slate-50 text-slate-700 hover:border-slate-200 hover:text-slate-800",
"flex whitespace-nowrap border-r py-3 px-3 text-xs focus:outline-none"
)
}
{...props}
>
{children}
</HeadlessTab>
);
}
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
export function UnderlinedList({ children, ...props }: HeadlessTabListProps) {
return (
<HeadlessTab.List
className={"-mb-px flex space-x-4 border-b border-slate-700"}
{...props}
>
{children}
</HeadlessTab.List>
);
}
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
export function Underlined({ children, ...props }: HeadlessTabProps) {
return (
<HeadlessTab
className={({ selected }: { selected: boolean }) =>
classnames(
selected
? "border-slate-300 text-slate-300 outline-none"
: "border-transparent text-slate-400 hover:border-slate-200 hover:text-slate-200",
"disabled:text-slate-300 disabled:hover:border-transparent",
"flex whitespace-nowrap border-b-2 py-2 px-4 text-base font-medium transition"
)
}
{...props}
>
{children}
</HeadlessTab>
);
}
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export function SegmentedList({
children,
className,
...props
}: HeadlessTabListProps) {
return (
<HeadlessTab.List
className={classNames(
"flex max-w-fit gap-0.5 rounded-md bg-slate-800 p-1",
className
)}
{...props}
>
{children}
</HeadlessTab.List>
);
}
export function Segmented({ children, ...props }: HeadlessTabProps) {
return (
<HeadlessTab
className={({ selected }: { selected: boolean }) =>
classnames(
selected
? "rounded bg-indigo-600 text-white shadow outline-none"
: "rounded text-slate-300 transition hover:bg-slate-700 hover:text-slate-300 hover:shadow-none",
"flex whitespace-nowrap py-2 px-4 text-xs font-medium"
)
}
{...props}
>
{children}
</HeadlessTab>
);
}
export function LargeBoxList({
children,
className,
...props
}: HeadlessTabListProps) {
return (
<HeadlessTab.List
className={classNames("grid grid-cols-carousel gap-2", className)}
{...props}
>
{children}
</HeadlessTab.List>
);
}
export function LargeBox({ children, ...props }: HeadlessTabProps) {
return (
<HeadlessTab
className={({ selected }: { selected: boolean }) =>
classnames(
selected
? "bg-slate-800 text-white shadow outline-none"
: "text-slate-300 transition hover:bg-slate-800/50 hover:text-slate-300 hover:shadow-none",
"flex flex-col items-center justify-center gap-4 rounded border border-slate-700 py-6 px-4 text-base font-medium"
)
}
{...props}
>
{children}
</HeadlessTab>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -1,21 +1,29 @@
import classNames from "classnames";
import React, { memo } from "react";
"use client";
export type TooltipProps = {
children: React.ReactNode;
text: string;
className?: string;
};
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "~/utils/cn";
export const Tooltip: React.FC<TooltipProps> = memo((props) => {
return (
<span className={classNames("group relative z-50 flex", props.className)}>
<span className="pointer-events-none absolute -top-10 left-1/2 flex -translate-x-1/2 items-center justify-center whitespace-nowrap rounded bg-slate-1000 px-2 py-1 text-xs text-slate-400 opacity-0 transition delay-300 duration-200 ease-in-out before:absolute before:left-1/2 before:top-full before:-translate-x-1/2 before:border-4 before:border-transparent before:border-t-black before:content-[''] group-hover:opacity-100">
{props.text}
</span>
{props.children}
</span>
);
});
const TooltipProvider = TooltipPrimitive.Provider;
Tooltip.displayName = "Tooltip";
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50",
className
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -4,10 +4,9 @@ import {
CheckCircleIcon,
ExclamationTriangleIcon,
} from "@heroicons/react/24/solid";
import classNames from "classnames";
import type { ReactNode } from "react";
import type { WorkflowRunStatus } from "~/models/workflowRun.server";
import { Spinner } from "../primitives/Spinner";
import { cn } from "~/utils/cn";
export function runStatusTitle(status: WorkflowRunStatus): string {
switch (status) {
@@ -53,7 +52,7 @@ export function runStatusIcon(
case "SUCCESS":
return (
<CheckCircleIcon
className={classNames(
className={cn(
iconSize === "small" ? smallClasses : largeClasses,
"relative text-green-500"
)}
@@ -62,7 +61,7 @@ export function runStatusIcon(
case "PENDING":
return (
<ClockIcon
className={classNames(
className={cn(
iconSize === "small" ? smallClasses : largeClasses,
"relative text-slate-500"
)}
@@ -71,16 +70,16 @@ export function runStatusIcon(
case "RUNNING":
return (
<Spinner
className={classNames(
className={cn(
iconSize === "small" ? smallClasses : largeClasses,
"relative text-blue-500 ml-[1px]"
"relative ml-[1px] text-blue-500"
)}
/>
);
case "DISCONNECTED":
return (
<ExclamationTriangleIcon
className={classNames(
className={cn(
iconSize === "small" ? smallClasses : largeClasses,
"relative text-amber-300"
)}
@@ -89,7 +88,7 @@ export function runStatusIcon(
case "ERROR":
return (
<XCircleIcon
className={classNames(
className={cn(
iconSize === "small" ? smallClasses : largeClasses,
"relative text-rose-500"
)}
@@ -98,7 +97,7 @@ export function runStatusIcon(
case "TIMED_OUT":
return (
<ExclamationTriangleIcon
className={classNames(
className={cn(
iconSize === "small" ? smallClasses : largeClasses,
"relative text-amber-300"
)}
@@ -1,29 +0,0 @@
export function customEvent(apiKey: string) {
return `import { customEvent, Trigger } from "@trigger.dev/sdk";
import { z } from "zod";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "user-created-notify-slack",
name: "User Created - Notify Slack",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
on: customEvent({
name: "user.created",
//todo define the schema for the events you want to receive
//this example accepts JSON like this: { id: "123", admin: false }
//you can use z.any() to accept any data, but you won't get payload validation or type inference in run()
schema: z.object({ id: z.string(), admin: z.boolean() }),
//todo define or remove the filter
//filters are optional, but can be used to filter out events
//this example stops the run function firing when data.admin === true
filter: {
admin: [false],
},
}),
run: async (event, ctx) => {
//insert your code here
},
}).listen();`;
}
@@ -1,28 +0,0 @@
export function githubIssues(apiKey: string) {
return `import { Trigger } from "@trigger.dev/sdk";
import * as github from "@trigger.dev/github";
import * as slack from "@trigger.dev/slack";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "github-issues-to-slack",
name: "Posts to Slack when a GitHub Issue created or modified",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
on: github.events.issueEvent({
//todo set your repo here
repo: "my-github-org/my-github-repo",
}),
run: async (event, ctx) => {
//we post a Slack message
const response = await slack.postMessage("send-to-slack", {
//todo set your Slack channel name here
channelName: "my-slack-channel-name",
text: \`A new issue has been created or modified. \${event.action}\`,
});
return response.message;
},
}).listen();`;
}
@@ -1,26 +0,0 @@
export function githubStars(apiKey: string) {
return `import { Trigger } from "@trigger.dev/sdk";
import * as github from "@trigger.dev/github";
import * as slack from "@trigger.dev/slack";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "new-github-star-to-slack",
name: "New GitHub Star: triggerdotdev/trigger.dev",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
on: github.events.newStarEvent({
//todo set your repo here
repo: "triggerdotdev/trigger.dev",
}),
run: async (event) => {
//we post a Slack message
await slack.postMessage("github-stars", {
//todo set your Slack channel name here
channelName: "github-stars",
text: \`New GitHub star from \n<\${event.sender.html_url}|\${event.sender.login}>\`,
});
},
}).listen();`;
}
@@ -1,35 +0,0 @@
export function newUserSlackMessage(apiKey: string) {
return `import { Trigger, customEvent } from "@trigger.dev/sdk";
import { postMessage } from "@trigger.dev/slack";
import { z } from "zod";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "new-user",
name: "New user slack message",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
on: customEvent({
name: "user.created",
schema: z.object({
name: z.string(),
email: z.string(),
paidPlan: z.boolean(),
}),
}),
run: async (event, ctx) => {
await ctx.logger.info("This log will appear on the Trigger.dev run page");
//send a message to the #new-users Slack channel with user details
const response = await postMessage("send-to-slack", {
channelName: "new-users",
text: \`New user: \${event.name} (\${event.email}) signed up. \${
event.paidPlan ? "They are paying" : "They are on the free plan"
}.\`,
});
return response.message;
},
}).listen();`;
}
@@ -1,119 +0,0 @@
export function resendEmailDripCampaign(apiKey: string) {
return `import { customEvent, Trigger } from "@trigger.dev/sdk";
import * as resend from "@trigger.dev/resend";
import * as slack from "@trigger.dev/slack";
import { Html } from "@react-email/html";
import { Preview } from "@react-email/preview";
import { Section } from "@react-email/section";
import { Text } from "@react-email/text";
import { z } from "zod";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "welcome-email-campaign",
name: "Welcome email drip campaign",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
on: customEvent({
name: "user.created",
schema: z.object({
userId: z.string(),
}),
}),
async run(event, context) {
//get the user data from the database
const user = await getUser(event.userId);
await slack.postMessage("send-to-slack", {
channelName: "new-users",
text: \`New user signed up: \${user.name} (\${user.email})\`,
});
//Send the first email
const welcomeResponse = await resend.sendEmail("welcome-email", {
from: "Trigger.dev <james@email.trigger.dev>",
replyTo: "James <james@trigger.dev>",
to: user.email,
subject: "Welcome to Trigger.dev",
react: <WelcomeEmail name={user.name} />,
});
await context.logger.debug(
\`Sent welcome email to \${welcomeResponse.to} with id \${welcomeResponse.id}
\`
);
//wait 1 day, check if the user has created a workflow and send the appropriate email
await context.waitFor("wait-a-while", { days: 1 });
const updatedUser = await getUser(event.userId);
if (updatedUser.hasOnboarded) {
await resend.sendEmail("onboarding-complete", {
from: "Trigger.dev <james@email.trigger.dev>",
replyTo: "James <james@trigger.dev>",
to: updatedUser.email,
subject: "Pro tips for workflows",
react: <TipsEmail name={updatedUser.name} />,
});
} else {
await resend.sendEmail("onboarding-incomplete", {
from: "Trigger.dev <james@email.trigger.dev>",
replyTo: "James <james@trigger.dev>",
to: updatedUser.email,
subject: "Help with your first workflow",
react: <InactiveEmail name={updatedUser.name} />,
});
}
},
}).listen();
function WelcomeEmail({ name }: { name?: string }) {
return (
<Html>
<Preview>This is the text that appears in the inbox list</Preview>
<Section>
<Text>Hey {name ?? "there"},</Text>
<Text>Your message goes here.</Text>
</Section>
</Html>
);
}
function TipsEmail({ name }: { name: string }) {
return (
<Html>
<Preview>This is the text that appears in the inbox list</Preview>
<Section>
<Text>Hi {name ?? "there"},</Text>
<Text>
Tips content goes here
</Text>
</Section>
</Html>
);
}
function InactiveEmail({ name }: { name: string }) {
return (
<Html>
<Preview>This is the text that appears in the inbox list</Preview>
<Section>
<Text>Hi {name ?? "there"},</Text>
<Text>
Re-engagement content goes here
</Text>
</Section>
</Html>
);
}
//This file is a mock database, in your real app you would access your real database from inside these functions
async function getUser(userId: string, hasOnboarded = false) {
return {
id: userId,
name: "Matt Aitken",
email: "matt@trigger.dev",
hasOnboarded,
};
}`;
}
@@ -1,135 +0,0 @@
import {
StarIcon,
ShoppingCartIcon,
ChatBubbleOvalLeftEllipsisIcon,
UserIcon,
DocumentTextIcon,
} from "@heroicons/react/24/outline";
import { customEvent } from "./custom-event";
import { githubIssues } from "./github-issues";
import { githubStars } from "./github-stars";
import { newUserSlackMessage } from "./new-user-slack-message";
import { shopifyCreateNewProducts } from "./shopify-create-new-product";
import { webhook } from "./webhook";
import { scheduled } from "./scheduled";
import { whatsappListenForMessageAndReply } from "./whatsapp-listen-for-message-and-reply";
import { scheduledCron } from "./scheduled-cron";
export type ExampleProject = {
name: string;
title?: string;
icon?: React.ReactNode;
docsLink?: string;
description: string;
requiredPackages: string;
code: (apiKey: string) => string;
packagesCopy?: string;
bulletPoints?: string[];
type: "example" | "from-scratch" | "blank";
};
export const allExamples: ExampleProject[] = [
{
icon: <StarIcon className="h-8 w-8 text-yellow-400" />,
name: "GitHub star → Slack",
title: "When you receive a GitHub star, post that user's details to Slack",
description:
"This workflow is triggered when a GitHub user adds a star to a repository. The user's details will then be posted in a specific Slack channel.",
requiredPackages: "@trigger.dev/sdk @trigger.dev/github @trigger.dev/slack",
code: githubStars,
type: "example",
},
{
icon: <UserIcon className="h-8 w-8 text-rose-400" />,
name: "New user → Slack",
title: "When a new user signs up, post a message to Slack",
description:
"This workflow is triggered when a new user signs up. The user's details will then be posted in a specific Slack channel.",
requiredPackages: "@trigger.dev/sdk @trigger.dev/slack zod",
code: newUserSlackMessage,
type: "example",
},
{
icon: <ShoppingCartIcon className="h-8 w-8 text-purple-400" />,
name: "New item → Shopify",
title: "When a custom event is triggered, create a new product in Shopify",
description:
"This workflow is triggered by a custom event. Once it is triggered, a new product is created in Shopify with the specified details.",
requiredPackages: "@trigger.dev/sdk @trigger.dev/shopify zod",
code: shopifyCreateNewProducts,
packagesCopy: "Shopify",
type: "example",
},
{
icon: <DocumentTextIcon className="h-8 w-8 text-orange-400" />,
name: "GitHub issue → Slack",
title: "When a GitHub issue is created or modified, post it to Slack",
description:
"This workflow is triggered when a new issue is created or modified in GitHub. The issue will then be posted in a specific Slack channel.",
requiredPackages: "@trigger.dev/sdk @trigger.dev/github @trigger.dev/slack",
code: githubIssues,
type: "example",
},
{
icon: <ChatBubbleOvalLeftEllipsisIcon className="h-8 w-8 text-green-400" />,
name: "WhatsApp → WhatsApp",
title: "Listen for WhatsApp messages and automatically reply",
description:
"This workflow is triggered when a WhatsApp message has been received. When received, a pre-determined reply is sent.",
requiredPackages: "@trigger.dev/sdk @trigger.dev/whatsapp",
code: whatsappListenForMessageAndReply,
packagesCopy: "WhatsApp",
type: "example",
},
{
name: "Webhook",
requiredPackages: "@trigger.dev/sdk zod",
code: webhook,
docsLink: "https://docs.trigger.dev/triggers/webhooks",
description:
"Webhooks allow you to subscribe to events from APIs but can be difficult to work with, especially when developing locally. Trigger.dev makes using webhooks easy:",
bulletPoints: [
"You dont need to register/unregister for webhooks, we do it for you.",
"They work locally during development without needing to use tunnels (e.g. Ngrok).",
"We receive the webhook, then keep trying to send it to you until you receive it. If your server goes down, no problem.",
],
type: "from-scratch",
},
{
name: "Custom event",
requiredPackages: "@trigger.dev/sdk zod",
code: customEvent,
docsLink: "https://docs.trigger.dev/triggers/custom-events",
description:
"Custom event triggers allow you to run workflows from your own code (or your other workflows). Send an event and any workflows that subscribe to that custom event will get triggered. You can easily send an event from anywhere, including from inside another workflow. Events dont have to come from the same server as your workflow and can be sent as HTTP requests from any language.",
type: "from-scratch",
},
{
name: "Scheduled (recurring)",
requiredPackages: "@trigger.dev/sdk",
code: scheduled,
docsLink: "https://docs.trigger.dev/triggers/scheduled",
description:
"Run a workflow on a recurring schedule. The example below will run every 5 minutes, starting 5 minutes after this code is first run on your server (that includes running locally).",
type: "from-scratch",
},
{
name: "Scheduled (CRON)",
requiredPackages: "@trigger.dev/sdk",
code: scheduledCron,
docsLink: "https://docs.trigger.dev/triggers/scheduled",
description:
"Run a workflow on a recurring schedule. The example job below will run at 2:30pm every Monday.",
type: "from-scratch",
},
];
export const exampleProjects = allExamples.filter(
(example) => example.type === "example"
);
export const fromScratchProjects = allExamples.filter(
(example) => example.type === "from-scratch"
);
@@ -1,26 +0,0 @@
export function scheduledCron(apiKey: string) {
return `import { Trigger, scheduleEvent } from "@trigger.dev/sdk";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "cron-scheduled-workflow",
name: "Cron Scheduled Workflow",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
//todo set your CRON expression here
//this example runs every Monday at 2:30pm UTC
//this site is useful when writing CRON: https://crontab.guru
//you don't have to wait to test, use our "Test" button on your workflow page
on: scheduleEvent({ cron: "30 14 * * 1" }),
run: async (event, ctx) => {
//this function is run every Monday at 2:30pm UTC
await ctx.logger.info("Received the cron scheduled event", {
event,
wallTime: new Date(),
});
return { foo: "bar" };
},
}).listen();`;
}
@@ -1,25 +0,0 @@
export function scheduled(apiKey: string) {
return `import { Trigger, scheduleEvent } from "@trigger.dev/sdk";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "scheduled-workflow",
name: "Scheduled Workflow",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
//todo set how often you want your event to fire here
//this example runs every 5 minutes, the first run will be 5 minutes after the workflow is first connected
//you don't have to wait to test, use our "Test" button on your workflow page
on: scheduleEvent({ rateOf: { minutes: 5 } }),
run: async (event, ctx) => {
//this function is run every 5 minutes
await ctx.logger.info("Received the scheduled event", {
event,
wallTime: new Date(),
});
return { foo: "bar" };
},
}).listen();`;
}
@@ -1,63 +0,0 @@
export function shopifyCreateNewProducts(apiKey: string) {
return `import { Trigger, customEvent } from "@trigger.dev/sdk";
import { z } from "zod";
import * as shopify from "@trigger.dev/shopify";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "shopify-products",
name: "Shopify products",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
//todo define the schema for the events you want to receive
//this example accepts an empty JSON object: {}
//you can use z.any() to accept any JSON, but you won't get nice types in the run function
on: customEvent({
name: "shopify.products",
schema: z.object({}),
}),
run: async (event, ctx) => {
//this creates a new product in your Shopify store, with a variant
const newProduct = await shopify.createProduct("create-product", {
descriptionHtml: "This is my brilliant <i>product description</i>.",
title: \`Fantastic product \${Math.floor(Math.random() * 1000)}\`,
productType: "t-shirt",
vendor: "Nike",
options: ["Color", "Size"],
standardizedProductType: {
//you may need to update this to match your store's product taxonomy
productTaxonomyNodeId: "gid://shopify/ProductTaxonomyNode/352",
},
variants: [
{
price: "99.99",
sku: "variant-1",
inventoryItem: {
tracked: true,
},
options: ["Maroon", "Tiny"],
},
],
});
//we add two images to the product
const newImages = await shopify.appendProductImages("append-images", {
id: newProduct.id,
images: [
{
src: "https://via.placeholder.com/600/92c952.png",
altText: "Image 1",
},
{
src: "https://via.placeholder.com/600/d32776.png",
altText: "Image 2",
},
],
});
return newProduct;
},
}).listen();
`;
}
@@ -1,34 +0,0 @@
export function webhook(apiKey: string) {
return `import { webhookEvent, Trigger } from "@trigger.dev/sdk";
import { z } from "zod";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "caldotcom-to-slack",
name: "Cal.com To Slack",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
//todo setup your custom webhook.
//we have integrations that make this much easier for supported APIs
on: webhookEvent({
service: "cal.com",
//the name of the event you want to subscribe for
eventName: "BOOKING_CREATED",
filter: {
triggerEvent: ["BOOKING_CREATED"],
},
//you can define a schema to validate the payload, and have nice types in the run function
//here we use z.any() to accept any payload
schema: z.any(),
//some webhooks are signed, set the header name and we'll verify the signature for you
verifyPayload: {
enabled: true,
header: "X-Cal-Signature-256",
},
}),
run: async (event, ctx) => {
//insert your code here
},
}).listen();`;
}
@@ -1,39 +0,0 @@
export function whatsappListenForMessageAndReply(apiKey: string) {
return `import { Trigger } from "@trigger.dev/sdk";
import { events, sendReaction, sendText } from "@trigger.dev/whatsapp";
new Trigger({
//todo: ensure this id is only used for this workflow
id: "whatsapp-webhook",
name: "Listen for WhatsApp messages and reply",
// For security, we recommend moving this api key to your .env / secrets file.
// Our env variable is called TRIGGER_API_KEY
apiKey: "${apiKey}",
//todo you need put in your WhatsApp Business account ID
on: events.messageEvent({
accountId: "<your_account_id>",
}),
run: async (event, ctx) => {
//these logs will appear on the run page
await ctx.logger.info(\`Message data\`, event.message);
await ctx.logger.info(\`Phone number\`, event.contacts[0]);
//add a 🥰 reaction to the original message
const reactionResponse = await sendReaction("reaction", {
fromId: event.metadata.phone_number_id,
to: event.message.from,
isReplyTo: event.message.id,
emoji: "🥰",
});
//send a text message in response
const textResponse = await sendText("text-msg", {
fromId: event.metadata.phone_number_id,
to: event.message.from,
text: "Hello! This is a text sent automatically from https://www.trigger.dev",
});
//we support all other types of WhatsApp messages (audio, video, document, location, etc)
},
}).listen();`;
}
@@ -1,52 +0,0 @@
import classNames from "classnames";
import type { TemplateListItem } from "~/presenters/templateListPresenter.server";
import { ApiLogoIcon } from "../code/ApiLogoIcon";
import { Body } from "../primitives/text/Body";
import { Header1 } from "../primitives/Headers";
export function TemplateCard({
template,
className,
}: {
template: TemplateListItem;
className?: string;
}) {
return (
<div
key={template.title}
className={classNames(
className,
"flex h-fit w-full flex-col overflow-hidden rounded-md border border-slate-700/50 bg-slate-1000 text-slate-300 shadow-md"
)}
>
<div className="h-36 w-full">
<img
src={template.imageUrl}
alt={template.shortTitle}
className="h-full w-full object-cover"
/>
</div>
<div className="flex flex-col p-5">
<div className="flex flex-col gap-y-2 ">
<Header1 size="extra-small" className="text-slate-300">
{template.title}
</Header1>
<Body size="small" className="text-slate-500">
{template.description}
</Body>
</div>
<div className="flex gap-x-1">
{template.services.map((service) => (
<div key={service.service} className="">
<ApiLogoIcon
integration={service}
size="regular"
className="mt-2 flex h-8 w-8 items-center justify-center rounded border-[1px] border-slate-700 bg-slate-900"
/>
</div>
))}
</div>
</div>
</div>
);
}
@@ -1,149 +0,0 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import classNames from "classnames";
import { Fragment } from "react";
import type { TemplateListItem } from "~/presenters/templateListPresenter.server";
import { ApiLogoIcon } from "../code/ApiLogoIcon";
import { CopyTextPanel } from "../CopyTextButton";
import { SecondaryA } from "../primitives/Buttons";
import { Body } from "../primitives/text/Body";
import { Header1 } from "../primitives/Headers";
export function TemplateOverview({
template,
className,
commandFlags,
}: {
template: TemplateListItem;
className?: string;
commandFlags?: string;
}) {
const { docsHTML, imageUrl } = template;
return (
<div
className={classNames(
className,
"grid w-full grid-cols-1 gap-8 rounded-lg bg-slate-850 pl-8 text-left lg:grid-cols-[24rem_minmax(0,_1fr)]"
)}
>
<div className="flex h-max flex-col rounded-r lg:sticky lg:top-4">
<TemplateDetails
template={template}
commandFlags={commandFlags}
className="hidden lg:flex"
/>
</div>
<div className="flex h-full w-full flex-col rounded">
<div className="hidden h-fit w-full transition group-hover:opacity-90 lg:block">
<img
src={imageUrl}
alt="Template hero"
className="h-full w-full rounded-t-md object-cover"
/>
</div>
<TemplateDetails template={template} className="lg:hidden" />
<div className="flex rounded-b bg-slate-900/75 p-8">
<div
className="prose prose-sm prose-invert min-w-full [&>pre]:bg-[rgb(17,23,41)]"
dangerouslySetInnerHTML={{
__html: docsHTML,
}}
/>
</div>
</div>
</div>
);
}
function TemplateDetails({
className,
template,
commandFlags,
}: {
className?: string;
template: TemplateListItem;
commandFlags?: string;
}) {
const { title, description, repositoryUrl, id } = template;
return (
<div className={classNames(className, "flex flex-col")}>
<Header1 size="extra-large" className="mt-2 mb-4 font-semibold">
{title}
</Header1>
<Body className="mb-6 text-slate-400">{description}</Body>
{template.services.length != 0 ? (
<>
<div className="flex items-center">
<Body
size="extra-small"
className="uppercase tracking-wide text-slate-500"
>
Integrations
</Body>
<div className="ml-2 h-px w-full bg-slate-800" />
</div>
<div className="mb-6 flex gap-x-1">
{template.services.map((service) => (
<Fragment key={service.service}>
<ApiLogoIcon
integration={service}
size="regular"
className="mt-2 flex h-8 w-8 items-center justify-center rounded border border-slate-800 bg-slate-900 transition group-hover:border-slate-600 group-hover:bg-slate-900/80"
/>
</Fragment>
))}
</div>
</>
) : (
<></>
)}
<div className="mb-2 flex items-center">
<Body
size="extra-small"
className="whitespace-nowrap uppercase tracking-wide text-slate-500"
>
Help and guides
</Body>
<div className="ml-2 h-px w-full bg-slate-800" />
</div>
<div className="mb-8 grid grid-cols-2 gap-2">
<SecondaryA
href={repositoryUrl}
target="_blank"
className="!max-w-full"
>
View Repo
<ArrowTopRightOnSquareIcon className="ml-1 h-4 w-4" />
</SecondaryA>
<SecondaryA
href="https://docs.trigger.dev"
target="_blank"
className="!max-w-full"
>
View Docs
<ArrowTopRightOnSquareIcon className="ml-1 h-4 w-4" />
</SecondaryA>
</div>
<div className="mb-2 flex items-center">
<Body
size="extra-small"
className="whitespace-nowrap uppercase tracking-wide text-slate-500"
>
Get started
</Body>
<div className="ml-2 h-px w-full bg-slate-800" />
</div>
<Body className="mb-4 text-slate-400">
Run this command in your terminal to create a new project using this
template.
</Body>
<CopyTextPanel
text={`npx create-trigger ${template.slug}`}
value={`npx create-trigger@latest ${template.slug} ${
commandFlags ? ` ${commandFlags}` : ""
}`}
className="mb-8"
/>
</div>
);
}
@@ -1,75 +0,0 @@
import GitHubStarsTemplateBg from "../../../public/images/templates/github-stars-template-bg.png";
import ResendSlackTemplateBg from "../../../public/images/templates/resend-slack-template-bg.png";
import ShopifyTemplateBg from "../../../public/images/templates/shopify-template-bg.png";
export type TemplateData = {
title: string;
shortTitle: string;
description: string;
imageURL: string;
githubRepoURL: string;
services: string[];
documentation?: string;
};
export const templateData = [
{
title: "Slack notifications when a GitHub repo is starred",
shortTitle: "GitHub stars to Slack",
description:
"When a GitHub repo is starred, post information about the user to Slack",
imageURL: GitHubStarsTemplateBg,
githubRepoURL: "repo-url",
services: ["slack", "github"],
documentation: `
### 🚀 Installation
Download the Mintlify CLI using the following command
\`\`\`
npm i -g mintlify
\`\`\`
### 👩💻 Development
Run the following command at the root of your Mintlify application to preview changes locally.
\`\`\`
mintlify dev
\`\`\`
Note - \`mintlify dev\` requires \`yarn\` and it's recommended you install it as a global installation. If you don't have yarn installed already run \`npm install --global yarn\` in your terminal.
### Custom Ports
Mintlify uses port 3000 by default. You can use the \`--port\` flag to customize the port Mintlify runs on. For example, use this command to run in port 3333:
\`\`\`
mintlify dev --port 3333
\`\`\`
You will see an error like this if you try to run Mintlify in a port that's already taken:
\`\`\`
Error: listen EADDRINUSE: address already in use :::3000
\`\`\`
`
},
{
title: "New user welcome email drip campaign",
shortTitle: "GitHub stars to Slack",
description: "Create a welcome email drip campaign using Slack and Resend",
imageURL: ResendSlackTemplateBg,
githubRepoURL: "repo-url",
services: ["slack", "github"],
},
{
title: "Add a new product to Shopify",
shortTitle: "GitHub stars to Slack",
description: "Add a new product to Shopify",
imageURL: ShopifyTemplateBg,
githubRepoURL: "repo-url",
services: ["slack", "github"],
},
];
@@ -1,124 +0,0 @@
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Link } from "@remix-run/react";
import classNames from "classnames";
import { Fragment, useState } from "react";
import type { TemplateListItem } from "~/presenters/templateListPresenter.server";
import { CopyTextPanel } from "../CopyTextButton";
import { StyledDialog } from "../primitives/Dialog";
import { Header1 } from "../primitives/Headers";
import { TemplateOverview } from "./TemplateOverview";
export function TemplatesGrid({
templates,
openInNewPage,
commandFlags,
}: {
templates: Array<TemplateListItem>;
openInNewPage: boolean;
commandFlags?: string;
}) {
const [openedTemplate, setOpenedTemplate] = useState<TemplateListItem | null>(
null
);
const isOpen = openedTemplate !== null;
return (
<>
<StyledDialog.Dialog
onClose={(e) => setOpenedTemplate(null)}
appear
show={isOpen}
as={Fragment}
>
<StyledDialog.Panel className="relative mx-auto flex max-h-[80vh] max-w-6xl items-start gap-2 overflow-hidden overflow-y-auto rounded-md border border-slate-700">
{openedTemplate && (
<TemplateOverview
template={openedTemplate}
commandFlags={commandFlags}
/>
)}
<button
onClick={() => setOpenedTemplate(null)}
className="group sticky top-2 -ml-[48px] rounded text-slate-400 transition hover:bg-slate-800/70 hover:text-slate-500"
>
<XMarkIcon className="h-8 w-8 transition group-hover:text-slate-300" />
</button>
</StyledDialog.Panel>
</StyledDialog.Dialog>
<div className="grid w-full grid-cols-1 items-start justify-start gap-5 lg:grid-cols-2 xl:grid-cols-3">
{templates.map((template) => {
return (
<TemplateButtonOrLink
key={template.slug}
template={template}
openInNewPage={openInNewPage}
onClick={() => setOpenedTemplate(template)}
className="p-5"
>
<div className="w-full transition group-hover:opacity-90 group-hover:shadow-lg">
<img
src={template.imageUrl}
alt={template.title}
className="h-full w-full rounded-md object-cover"
/>
</div>
<div className="flex h-full w-full flex-col justify-between">
<Header1 size="regular" className="py-6 text-slate-100">
{template.title}
</Header1>
<CopyTextPanel
value={`npx create-trigger@latest ${template.slug}${
commandFlags ? ` ${commandFlags}` : ``
}`}
text={`npx create-trigger ${template.slug}`}
className=""
/>
</div>
</TemplateButtonOrLink>
);
})}
</div>
</>
);
}
function TemplateButtonOrLink({
template,
openInNewPage,
onClick,
children,
className,
}: {
template: TemplateListItem;
openInNewPage: boolean;
onClick: (e: React.MouseEvent) => void;
children: React.ReactNode;
className?: string;
}) {
const cardStyles =
"group flex w-full p-5 flex-col self-stretch overflow-hidden rounded-md border border-slate-700/70 bg-slate-800 text-left text-slate-200 shadow-md transition hover:cursor-pointer hover:border-slate-600 hover:bg-slate-700/50 disabled:opacity-50";
if (openInNewPage) {
return (
<Link
to={template.slug}
prefetch="intent"
reloadDocument
className={classNames(cardStyles, className)}
>
{children}
</Link>
);
} else {
return (
<button
key={template.title}
type="button"
onClick={onClick}
className={cardStyles}
>
{children}
</button>
);
}
}
@@ -1,198 +0,0 @@
import type {
CustomEventTrigger,
ScheduledEventTrigger,
ScheduleSourceCron,
ScheduleSourceRate,
SlackInteractionEventTrigger,
TriggerMetadata,
WebhookEventTrigger,
} from "@trigger.dev/internal";
import { Body } from "../primitives/text/Body";
import { Header2 } from "../primitives/Headers";
import cronstrue from "cronstrue";
export function TriggerBody({ trigger }: { trigger: TriggerMetadata }) {
switch (trigger.type) {
case "WEBHOOK":
return <Webhook webhook={trigger} />;
case "SCHEDULE":
return <Scheduled event={trigger} />;
case "CUSTOM_EVENT":
return <CustomEvent event={trigger} />;
case "HTTP_ENDPOINT":
break;
case "SLACK_INTERACTION":
return <SlackInteraction trigger={trigger} />;
default:
break;
}
return <></>;
}
const workflowNodeUppercaseClasses = "uppercase text-slate-400 tracking-wide";
function SlackInteraction({
trigger,
}: {
trigger: SlackInteractionEventTrigger;
}) {
return (
<div className="flex flex-col">
<div className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Name
</Body>
<Header2 size="small" className="mb-2 text-slate-300">
{trigger.name}
</Header2>
</div>
{trigger.source.type === "block_action" && (
<div className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Block
</Body>
<Header2 size="small" className="mb-2 text-slate-300">
{trigger.source.blockId}
</Header2>
</div>
)}
{trigger.source.type === "block_action" && (
<div className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Action
</Body>
<Header2 size="small" className="mb-2 text-slate-300">
{trigger.source.actionIds.join(", ")}
</Header2>
</div>
)}
{trigger.source.type === "view_submission" && (
<div className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Callback IDs
</Body>
<Header2 size="small" className="mb-2 text-slate-300">
{trigger.source.callbackIds.join(", ")}
</Header2>
</div>
)}
</div>
);
}
// trigger.source.actionIds;
function Webhook({ webhook }: { webhook: WebhookEventTrigger }) {
return (
<>
<Header2 size="small" className="mb-2 text-slate-300">
{webhook.name}
</Header2>
<div className="flex flex-col gap-1">
{webhook.source &&
!webhook.manualRegistration &&
Object.entries(webhook.source).map(([key, value]) => (
<div key={key} className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
{key}
</Body>
<Body size="small">{value}</Body>
</div>
))}
</div>
</>
);
}
function CustomEvent({ event }: { event: CustomEventTrigger }) {
return (
<>
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Name
</Body>
<Header2 size="small" className="mb-2 text-slate-300">
{event.name}
</Header2>
</>
);
}
function Scheduled({ event }: { event: ScheduledEventTrigger }) {
return (
<>
<div className={workflowNodeUppercaseClasses}>
{"rateOf" in event.source ? (
<RateOfScheduled source={event.source} />
) : (
<AtScheduled source={event.source} />
)}
</div>
</>
);
}
function RateOfScheduled({ source }: { source: ScheduleSourceRate }) {
const unit =
"minutes" in source.rateOf
? source.rateOf.minutes > 1
? "minutes"
: "minute"
: "hours" in source.rateOf
? source.rateOf.hours > 1
? "hours"
: "hour"
: source.rateOf.days > 1
? "days"
: "day";
const value =
"minutes" in source.rateOf
? source.rateOf.minutes
: "hours" in source.rateOf
? source.rateOf.hours
: source.rateOf.days;
return (
<div className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Runs
</Body>
<Body size="small" className="normal-case tracking-normal text-slate-300">
Every {value} {unit}
</Body>
</div>
);
}
function AtScheduled({ source }: { source: ScheduleSourceCron }) {
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Runs
</Body>
<Body
size="small"
className="normal-case tracking-normal text-slate-300"
>
{cronstrue.toString(source.cron, {
throwExceptionOnParseError: false,
verbose: false,
use24HourTimeFormat: true,
})}
</Body>
</div>
<div className="flex items-baseline gap-2">
<Body size="extra-small" className={workflowNodeUppercaseClasses}>
Cron expression
</Body>
<Body
size="small"
className="normal-case tracking-normal text-slate-300"
>
{source.cron}
</Body>
</div>
</div>
);
}
@@ -1,70 +0,0 @@
import { ApiLogoIcon } from "../code/ApiLogoIcon";
import CustomEvent from "../../assets/images/triggers/custom-event.png";
import HttpEndpoint from "../../assets/images/triggers/http-endpoint.png";
import Schedule from "../../assets/images/triggers/schedule.png";
import Webhook from "../../assets/images/triggers/webhook.png";
import SlackInteraction from "../../assets/images/triggers/slack-interaction.png";
import { triggerLabel } from "./triggerLabel";
type TriggerType =
| "CUSTOM_EVENT"
| "HTTP_ENDPOINT"
| "SCHEDULE"
| "WEBHOOK"
| "SLACK_INTERACTION";
const iconClasses = "h-full w-full";
export function TriggerTypeIcon({
type,
provider,
}: {
type: TriggerType;
provider?: { icon: string; name: string };
}) {
if (provider) {
return (
<ApiLogoIcon
integration={provider}
size="custom"
className={iconClasses}
/>
);
}
switch (type) {
case "CUSTOM_EVENT":
return (
<img
src={CustomEvent}
alt={triggerLabel(type)}
className={iconClasses}
/>
);
case "HTTP_ENDPOINT":
return (
<img
src={HttpEndpoint}
alt={triggerLabel(type)}
className={iconClasses}
/>
);
case "SCHEDULE":
return (
<img src={Schedule} alt={triggerLabel(type)} className={iconClasses} />
);
case "WEBHOOK":
return (
<img src={Webhook} alt={triggerLabel(type)} className={iconClasses} />
);
case "SLACK_INTERACTION":
return (
<img
src={SlackInteraction}
alt={triggerLabel(type)}
className={iconClasses}
/>
);
default:
return null;
}
}
@@ -1,23 +0,0 @@
type TriggerType =
| "CUSTOM_EVENT"
| "HTTP_ENDPOINT"
| "SCHEDULE"
| "WEBHOOK"
| "SLACK_INTERACTION";
export function triggerLabel(type: TriggerType) {
switch (type) {
case "CUSTOM_EVENT":
return "Custom event";
case "WEBHOOK":
return "Webhook";
case "HTTP_ENDPOINT":
return "HTTP endpoint";
case "SCHEDULE":
return "Scheduled";
case "SLACK_INTERACTION":
return "Slack interaction";
default:
return type;
}
}
@@ -3,7 +3,7 @@ import type { LoaderArgs } from "@remix-run/server-runtime";
import { SliderButton } from "@typeform/embed-react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { NamedIcon, NamedIconInBox } from "~/components/Icon";
import { NamedIcon, NamedIconInBox } from "~/components/primitives/NamedIcon";
import { ConnectButton } from "~/components/integrations/ConnectButton";
import { AppBody, AppLayoutTwoCol } from "~/components/layout/AppLayout";
import { Container } from "~/components/layout/Container";
+3
View File
@@ -48,6 +48,7 @@
"@aws-sdk/s3-request-presigner": "^3.186.0",
"@cakework/client": "^0.0.54",
"@cfworker/json-schema": "^1.12.5",
"@code-hike/mdx": "^0.8.3",
"@codemirror/autocomplete": "^6.3.1",
"@codemirror/commands": "^6.1.2",
"@codemirror/lang-javascript": "^6.1.1",
@@ -70,6 +71,8 @@
"@radix-ui/react-label": "^2.0.1",
"@radix-ui/react-popover": "^1.0.5",
"@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.5",
"@react-email/head": "^0.0.2",
"@remix-run/express": "v1.11.0",
"@remix-run/node": "v1.11.0",
+103 -1
View File
@@ -46,6 +46,7 @@ importers:
'@babel/preset-typescript': ^7.21.4
'@cakework/client': ^0.0.54
'@cfworker/json-schema': ^1.12.5
'@code-hike/mdx': ^0.8.3
'@codemirror/autocomplete': ^6.3.1
'@codemirror/commands': ^6.1.2
'@codemirror/lang-javascript': ^6.1.1
@@ -71,6 +72,8 @@ importers:
'@radix-ui/react-label': ^2.0.1
'@radix-ui/react-popover': ^1.0.5
'@radix-ui/react-select': ^1.2.1
'@radix-ui/react-tabs': ^1.0.3
'@radix-ui/react-tooltip': ^1.0.5
'@react-email/head': ^0.0.2
'@remix-run/dev': v1.11.0
'@remix-run/eslint-config': v1.11.0
@@ -231,6 +234,7 @@ importers:
'@aws-sdk/s3-request-presigner': 3.245.0
'@cakework/client': 0.0.54
'@cfworker/json-schema': 1.12.5
'@code-hike/mdx': 0.8.3_react@18.2.0
'@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/commands': 6.1.3
'@codemirror/lang-javascript': 6.1.2
@@ -253,6 +257,8 @@ importers:
'@radix-ui/react-label': 2.0.1_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-popover': 1.0.5_ib3m5ricvtkl2cll7qpr2f6lvq
'@radix-ui/react-select': 1.2.1_ib3m5ricvtkl2cll7qpr2f6lvq
'@radix-ui/react-tabs': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-tooltip': 1.0.5_ib3m5ricvtkl2cll7qpr2f6lvq
'@react-email/head': 0.0.2
'@remix-run/express': 1.11.0_cwk4saenierp7pa7l5cpbeswge
'@remix-run/node': 1.11.0_biqbaboplfbrettd7655fr4n2y
@@ -4495,6 +4501,23 @@ packages:
prettier: 2.8.2
dev: false
/@code-hike/lighter/0.6.4:
resolution: {integrity: sha512-RGH/15WnFecNSPUxMVmbql0py25ijrhQlMtqkOSIgVZtmHxh00DkqImx65uL3dADFIzOa0RkN3ObUJWAppOgJQ==}
dev: false
/@code-hike/mdx/0.8.3_react@18.2.0:
resolution: {integrity: sha512-pbbv7PivrU+GqPiM0ufehNyhsoge8V25fx+y89M2yKgEWMAFnNkk4E1XaW/X81QzIq3h58IoKEWnNYSJpERTvA==}
peerDependencies:
react: ^16.8.3 || ^17 || ^18
dependencies:
'@code-hike/lighter': 0.6.4
node-fetch: 2.6.7
react: 18.2.0
shiki: 0.10.1
transitivePeerDependencies:
- encoding
dev: false
/@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde:
resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==}
peerDependencies:
@@ -6725,6 +6748,26 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-roving-focus/1.0.3_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.7
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-collection': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
'@radix-ui/react-context': 1.0.0_react@18.2.0
'@radix-ui/react-direction': 1.0.0_react@18.2.0
'@radix-ui/react-id': 1.0.0_react@18.2.0
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-use-callback-ref': 1.0.0_react@18.2.0
'@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-select/1.2.1_ib3m5ricvtkl2cll7qpr2f6lvq:
resolution: {integrity: sha512-GULRMITaOHNj79BZvQs3iZO0+f2IgI8g5HDhMi7Bnc13t7IlG86NFtOCfTLme4PNZdEtU+no+oGgcl6IFiphpQ==}
peerDependencies:
@@ -6779,6 +6822,50 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-tabs/1.0.3_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-4CkF/Rx1GcrusI/JZ1Rvyx4okGUs6wEenWA0RG/N+CwkRhTy7t54y7BLsWUXrAz/GRbBfHQg/Odfs/RoW0CiRA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.7
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-context': 1.0.0_react@18.2.0
'@radix-ui/react-direction': 1.0.0_react@18.2.0
'@radix-ui/react-id': 1.0.0_react@18.2.0
'@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-roving-focus': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-tooltip/1.0.5_ib3m5ricvtkl2cll7qpr2f6lvq:
resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.7
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
'@radix-ui/react-context': 1.0.0_react@18.2.0
'@radix-ui/react-dismissable-layer': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-id': 1.0.0_react@18.2.0
'@radix-ui/react-popper': 1.1.1_ib3m5ricvtkl2cll7qpr2f6lvq
'@radix-ui/react-portal': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-slot': 1.0.1_react@18.2.0
'@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0
'@radix-ui/react-visually-hidden': 1.0.2_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-use-callback-ref/1.0.0_react@18.2.0:
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==}
peerDependencies:
@@ -16222,7 +16309,6 @@ packages:
/jsonc-parser/3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
/jsonfile/4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
@@ -20372,6 +20458,14 @@ packages:
interpret: 1.4.0
rechoir: 0.6.2
/shiki/0.10.1:
resolution: {integrity: sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==}
dependencies:
jsonc-parser: 3.2.0
vscode-oniguruma: 1.7.0
vscode-textmate: 5.2.0
dev: false
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@@ -22680,6 +22774,14 @@ packages:
acorn-walk: 8.2.0
dev: true
/vscode-oniguruma/1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: false
/vscode-textmate/5.2.0:
resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==}
dev: false
/w3c-keyname/2.2.6:
resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==}
dev: false