Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5910339bf4 | |||
| 4b641b25a6 | |||
| a847b492bc | |||
| 0df6410561 | |||
| c17faabe9f | |||
| 9a187f9eda | |||
| 2e9452ab7c | |||
| 914745f642 | |||
| 9189bdf50f | |||
| 3e5a97c4b5 | |||
| c0aa6633d2 | |||
| 4e35871861 | |||
| 90f52de147 | |||
| 28b05a82d8 | |||
| b9ed7e2ced | |||
| 5e651d84af | |||
| 6a992a1995 | |||
| 81e886a1ba | |||
| ab9e4a989c | |||
| e350659e24 | |||
| f888a49555 | |||
| 421c249e50 | |||
| a8a6f51387 | |||
| 12e73eef22 | |||
| 2e33fcb16b | |||
| 5912cdd11c | |||
| cc016b3ae3 | |||
| 7760e09462 | |||
| 3ca4456c88 | |||
| 618b7f22da | |||
| a12c7c3b0a | |||
| a42e94c75f | |||
| bc757c8ddb | |||
| 6e11ab9183 | |||
| 2397fcb640 | |||
| 35d0c2a06f | |||
| 813ec74672 | |||
| 03db13171a | |||
| 0ecb5129e8 | |||
| 44cb28c1c4 | |||
| 4578f6bd64 | |||
| 8b25e57613 | |||
| 8fb9ea19a3 | |||
| eb4ca0ce2d | |||
| df24cd5b71 |
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask a Question
|
||||
url: https://trigger.dev/discord
|
||||
|
||||
@@ -106,7 +106,7 @@ export function TriggerDevStep() {
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
<Paragraph spacing variant="small">
|
||||
If you’re not running on port 3000 you can specify the port by adding{" "}
|
||||
If you’re not running on the default you can specify the port by adding{" "}
|
||||
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
ClientTabsContent,
|
||||
} from "../primitives/ClientTabs";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
|
||||
type InstallPackagesProps = {
|
||||
packages: string[];
|
||||
};
|
||||
|
||||
export function InstallPackages({ packages }: InstallPackagesProps) {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npm install ${packages.join(" ")}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm install ${packages.join(" ")}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn add ${packages.join(" ")}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import type { ViewUpdate } from "@codemirror/view";
|
||||
import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid";
|
||||
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { getEditorSetup } from "./codeMirrorSetup";
|
||||
import { darkTheme } from "./codeMirrorTheme";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
defaultValue?: string;
|
||||
@@ -14,6 +16,8 @@ export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
onChange?: (value: string) => void;
|
||||
onUpdate?: (update: ViewUpdate) => void;
|
||||
onBlur?: (code: string) => void;
|
||||
showCopyButton?: boolean;
|
||||
showClearButton?: boolean;
|
||||
}
|
||||
|
||||
const languages = {
|
||||
@@ -38,6 +42,8 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
onBlur,
|
||||
basicSetup,
|
||||
autoFocus,
|
||||
showCopyButton = true,
|
||||
showClearButton = true,
|
||||
} = {
|
||||
...defaultProps,
|
||||
...opts,
|
||||
@@ -65,7 +71,8 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
onChange,
|
||||
onUpdate,
|
||||
};
|
||||
const { setContainer, state } = useCodeMirror(settings);
|
||||
const { setContainer, view } = useCodeMirror(settings);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
@@ -75,24 +82,71 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
|
||||
//if the defaultValue changes update the editor
|
||||
useEffect(() => {
|
||||
if (state !== undefined) {
|
||||
state.update({
|
||||
changes: { from: 0, to: state.doc.length, insert: defaultValue },
|
||||
if (view !== undefined) {
|
||||
if (view.state.doc.toString() === defaultValue) return;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: defaultValue },
|
||||
});
|
||||
}
|
||||
}, [defaultValue, state]);
|
||||
}, [defaultValue, view]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
if (view === undefined) return;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: undefined },
|
||||
});
|
||||
onChange?.("");
|
||||
}, [view]);
|
||||
|
||||
const copy = useCallback(() => {
|
||||
if (view === undefined) return;
|
||||
navigator.clipboard.writeText(view.state.doc.toString());
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1500);
|
||||
}, [view]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
|
||||
opts.className
|
||||
)}
|
||||
ref={editor}
|
||||
onBlur={() => {
|
||||
if (!onBlur) return;
|
||||
onBlur(editor.current?.textContent ?? "");
|
||||
}}
|
||||
/>
|
||||
<div className={cn(opts.className, "relative")}>
|
||||
<div
|
||||
className="h-full w-full"
|
||||
ref={editor}
|
||||
onBlur={() => {
|
||||
if (!onBlur) return;
|
||||
onBlur(editor.current?.textContent ?? "");
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-3 top-3 flex items-center gap-2">
|
||||
{showClearButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary/small"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clear();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
{showCopyButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary/small"
|
||||
LeadingIcon={copied ? CheckIcon : ClipboardIcon}
|
||||
leadingIconClassName={copied ? "text-green-500 group-hover:text-green-500" : undefined}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import {
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
keymap,
|
||||
} from "@codemirror/view";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import { closeBrackets } from "@codemirror/autocomplete";
|
||||
import { bracketMatching } from "@codemirror/language";
|
||||
import { indentWithTab } from "@codemirror/commands";
|
||||
|
||||
export function getPreviewSetup(): Array<Extension> {
|
||||
return [
|
||||
jsonLang(),
|
||||
highlightSpecialChars(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
highlightSelectionMatches(),
|
||||
lineNumbers(),
|
||||
];
|
||||
}
|
||||
|
||||
export function getViewerSetup(): Array<Extension> {
|
||||
return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()];
|
||||
}
|
||||
import { jsonParseLinter } from "@codemirror/lang-json";
|
||||
import { bracketMatching } from "@codemirror/language";
|
||||
import { lintGutter, lintKeymap, linter } from "@codemirror/lint";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
import { Prec, type Extension } from "@codemirror/state";
|
||||
import {
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
} from "@codemirror/view";
|
||||
|
||||
export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array<Extension> {
|
||||
const options = [
|
||||
@@ -36,7 +21,20 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
keymap.of([indentWithTab]),
|
||||
lintGutter(),
|
||||
linter(jsonParseLinter()),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{
|
||||
key: "Mod-Enter",
|
||||
run: () => {
|
||||
return true;
|
||||
},
|
||||
preventDefault: false,
|
||||
},
|
||||
])
|
||||
),
|
||||
keymap.of([indentWithTab, ...lintKeymap]),
|
||||
];
|
||||
|
||||
if (showLineNumbers) {
|
||||
|
||||
@@ -17,10 +17,15 @@ export function darkTheme(): Extension {
|
||||
violet = "#c678dd",
|
||||
darkBackground = "#21252b",
|
||||
highlightBackground = "rgba(71,85,105,0.2)",
|
||||
background = "#0f172a",
|
||||
background = "rgba(11, 16, 24 ,100)",
|
||||
tooltipBackground = "#353a42",
|
||||
selection = "rgb(71 85 105)",
|
||||
cursor = "#528bff";
|
||||
cursor = "#528bff",
|
||||
scrollbarTrack = "#0E1521",
|
||||
scrollbarTrackActive = "#131B2B",
|
||||
scrollbarThumb = "#293649",
|
||||
scrollbarThumbActive = "#3C4B62",
|
||||
scrollbarBg = "#0E1521";
|
||||
|
||||
const jsonHeroEditorTheme = EditorView.theme(
|
||||
{
|
||||
@@ -94,6 +99,45 @@ export function darkTheme(): Extension {
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
".cm-scroller": {
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: `${scrollbarThumb} ${scrollbarTrack}`,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar": {
|
||||
display: "block",
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-track": {
|
||||
backgroundColor: scrollbarTrack,
|
||||
borderRadius: "0",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-track:hover": {
|
||||
backgroundColor: scrollbarTrackActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-track:active": {
|
||||
backgroundColor: scrollbarTrackActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: scrollbarThumb,
|
||||
borderRadius: "0",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: scrollbarThumbActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-thumb:active": {
|
||||
backgroundColor: scrollbarThumbActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-corner": {
|
||||
backgroundColor: scrollbarBg,
|
||||
borderRadius: "0",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-corner:hover": {
|
||||
backgroundColor: scrollbarBg,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-corner:active": {
|
||||
backgroundColor: scrollbarBg,
|
||||
},
|
||||
},
|
||||
{ dark: true }
|
||||
);
|
||||
@@ -155,157 +199,3 @@ export function darkTheme(): Extension {
|
||||
|
||||
return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)];
|
||||
}
|
||||
|
||||
export function lightTheme(): Extension[] {
|
||||
const stringColor = "text-[#53a053]",
|
||||
numberColor = "text-[#447bef]",
|
||||
variableColor = "text-[#a42ea2]",
|
||||
booleanColor = "text-[#e2574e]",
|
||||
coral = "text-[#e06c75]",
|
||||
invalid = "text-[#ffffff]",
|
||||
ivory = "text-[#abb2bf]",
|
||||
stone = "text-[#7d8799]",
|
||||
malibu = "text-[#61afef]",
|
||||
whiskey = "text-[#d19a66]",
|
||||
violet = "text-[#c678dd]",
|
||||
darkBackground = "text-[#21252b]",
|
||||
highlightBackground = "text-[#D0D0D0]",
|
||||
background = "text-[#ffffff]",
|
||||
tooltipBackground = "text-[#353a42]",
|
||||
selection = "text-[#D0D0D0]",
|
||||
cursor = "text-[#528bff]";
|
||||
|
||||
const jsonHeroEditorTheme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
color: ivory,
|
||||
backgroundColor: background,
|
||||
},
|
||||
|
||||
".cm-content": {
|
||||
caretColor: cursor,
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
},
|
||||
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
".cm-panels": { backgroundColor: darkBackground, color: ivory },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
|
||||
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "#72a1ff59",
|
||||
outline: "1px solid #457dff",
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "#6199ff2f",
|
||||
},
|
||||
|
||||
".cm-activeLine": { backgroundColor: highlightBackground },
|
||||
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
|
||||
|
||||
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
|
||||
backgroundColor: "#bad0f847",
|
||||
outline: "1px solid #515a6b",
|
||||
},
|
||||
|
||||
".cm-gutters": {
|
||||
backgroundColor: background,
|
||||
color: stone,
|
||||
border: "none",
|
||||
},
|
||||
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "#ddd",
|
||||
},
|
||||
|
||||
".cm-tooltip": {
|
||||
border: "none",
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||
borderTopColor: "transparent",
|
||||
borderBottomColor: "transparent",
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: highlightBackground,
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: false }
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the JSON Hero theme.
|
||||
const jsonHeroHighlightStyle = tagHighlighter([
|
||||
{ tag: tags.keyword, class: violet },
|
||||
{
|
||||
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||
class: variableColor,
|
||||
},
|
||||
{
|
||||
tag: [tags.function(tags.variableName), tags.labelName],
|
||||
class: malibu,
|
||||
},
|
||||
{
|
||||
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
|
||||
class: whiskey,
|
||||
},
|
||||
{ tag: [tags.definition(tags.name), tags.separator], class: ivory },
|
||||
{
|
||||
tag: [
|
||||
tags.typeName,
|
||||
tags.className,
|
||||
tags.number,
|
||||
tags.changed,
|
||||
tags.annotation,
|
||||
tags.modifier,
|
||||
tags.self,
|
||||
tags.namespace,
|
||||
],
|
||||
class: numberColor,
|
||||
},
|
||||
{
|
||||
tag: [
|
||||
tags.operator,
|
||||
tags.operatorKeyword,
|
||||
tags.url,
|
||||
tags.escape,
|
||||
tags.regexp,
|
||||
tags.link,
|
||||
tags.special(tags.string),
|
||||
],
|
||||
class: stringColor,
|
||||
},
|
||||
{ tag: [tags.meta, tags.comment], class: stone },
|
||||
|
||||
{ tag: tags.link, class: stone },
|
||||
{ tag: tags.heading, class: coral },
|
||||
{
|
||||
tag: [tags.atom, tags.bool, tags.special(tags.variableName)],
|
||||
class: booleanColor,
|
||||
},
|
||||
{
|
||||
tag: [tags.processingInstruction, tags.string, tags.inserted],
|
||||
class: stringColor,
|
||||
},
|
||||
{ tag: tags.invalid, class: invalid },
|
||||
]);
|
||||
|
||||
return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)];
|
||||
}
|
||||
|
||||
@@ -51,18 +51,18 @@ export function FrameworkSelector() {
|
||||
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
|
||||
<NextjsLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupExpressPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupExpressPath(organization, project)} supported>
|
||||
<ExpressLogo className="w-36" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRemixPath(organization, project)} supported>
|
||||
<RemixLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
|
||||
<RedwoodLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupAstroPath(organization, project)} supported>
|
||||
<AstroLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
|
||||
<RedwoodLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
|
||||
<NuxtLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
@@ -72,7 +72,7 @@ export function FrameworkSelector() {
|
||||
<FrameworkLink to={projectSetupFastifyPath(organization, project)}>
|
||||
<FastifyLogo className="w-36" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupNestjsPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupNestjsPath(organization, project)} supported>
|
||||
<NestjsLogo className="w-36" />
|
||||
</FrameworkLink>
|
||||
</div>
|
||||
|
||||
@@ -79,37 +79,6 @@ export function HowToRunYourJob() {
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToRunATest() {
|
||||
return (
|
||||
<>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Select an environment
|
||||
"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>Select the environment you’d like the test to run against.</Paragraph>
|
||||
<img src={selectEnvironment} className="mt-2 w-52" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Write your test payload" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Write your own payload specific to your Job. Some Triggers also provide example payloads
|
||||
that you can select from. This will populate the code editor below.
|
||||
</Paragraph>
|
||||
<img src={selectExample} className="mt-2 h-40" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run your test" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>When you’re happy with the payload, click Run test.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Callout variant="docs" to="https://trigger.dev/docs/documentation/guides/testing-jobs">
|
||||
Learn more about running tests.
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToConnectAnIntegration() {
|
||||
return (
|
||||
<>
|
||||
@@ -272,6 +241,21 @@ export function HowToUseApiKeysAndEndpoints() {
|
||||
you should use the Test feature to trigger any scheduled Jobs.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Staging</span>
|
||||
<EnvironmentLabel environment={{ type: "STAGING" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
The <InlineCode>STAGING</InlineCode> environment is where your Jobs will run in a staging
|
||||
environment, meant to mirror your production environment.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
|
||||
@@ -119,11 +119,11 @@ export function ProjectSideMenu() {
|
||||
data-action="onboarding"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Homepage"
|
||||
icon="external-link"
|
||||
to="https://trigger.dev"
|
||||
name="Changelog"
|
||||
icon="list"
|
||||
to="https://trigger.dev/changelog"
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="onboarding"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
|
||||
@@ -144,7 +144,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
const textColorClassName = variation.textColor;
|
||||
|
||||
return (
|
||||
<div className={cn(fullWidth ? "flex" : "inline-flex text-xxs", btnClassName, className)}>
|
||||
<div className={cn("flex", fullWidth ? "" : "w-fit text-xxs", btnClassName, className)}>
|
||||
<div
|
||||
className={cn(
|
||||
textAlignLeft ? "text-left" : "justify-center",
|
||||
|
||||
@@ -63,16 +63,17 @@ export const DateTimeAccurate = ({ date, timeZone = "UTC" }: DateTimeProps) => {
|
||||
};
|
||||
|
||||
function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string {
|
||||
const milliseconds = `00${date.getMilliseconds()}`.slice(-3);
|
||||
|
||||
const formattedDateTime = new Intl.DateTimeFormat(locales, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
timeZone,
|
||||
// @ts-ignore this works in 92.5% of browsers https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_options_parameter_options_fractionalseconddigits_parameter
|
||||
fractionalSecondDigits: 3,
|
||||
}).format(date);
|
||||
|
||||
return `${formatDateTime}.${milliseconds}`;
|
||||
return formattedDateTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Icon, IconInBox, RenderIcon } from "./Icon";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const variations = {
|
||||
small: {
|
||||
label: {
|
||||
variant: "small" as const,
|
||||
className: "m-0 leading-[1.1rem]",
|
||||
},
|
||||
description: {
|
||||
variant: "extra-small" as const,
|
||||
className: "m-0",
|
||||
},
|
||||
},
|
||||
base: {
|
||||
label: {
|
||||
variant: "base" as const,
|
||||
className: "m-0 leading-[1.1rem] ",
|
||||
},
|
||||
description: {
|
||||
variant: "small" as const,
|
||||
className: "m-0",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type DetailCellProps = {
|
||||
leadingIcon?: RenderIcon;
|
||||
leadingIconClassName?: string;
|
||||
trailingIcon?: RenderIcon;
|
||||
trailingIconClassName?: string;
|
||||
label: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
className?: string;
|
||||
variant?: keyof typeof variations;
|
||||
};
|
||||
|
||||
export function DetailCell({
|
||||
leadingIcon,
|
||||
leadingIconClassName,
|
||||
trailingIcon,
|
||||
trailingIconClassName,
|
||||
label,
|
||||
description,
|
||||
className,
|
||||
variant = "small",
|
||||
}: DetailCellProps) {
|
||||
const variation = variations[variant];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-900",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<IconInBox
|
||||
icon={leadingIcon}
|
||||
className={cn("flex-none transition group-hover:border-slate-750", leadingIconClassName)}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<Paragraph
|
||||
variant={variation.label.variant}
|
||||
className={cn(
|
||||
"flex-1 text-left transition group-hover:text-bright",
|
||||
variation.label.className
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Paragraph>
|
||||
{description && (
|
||||
<Paragraph
|
||||
variant={variation.description.variant}
|
||||
className={cn(
|
||||
"flex-1 text-left text-dimmed transition group-hover:text-bright",
|
||||
variation.description.className
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<Icon
|
||||
icon={trailingIcon}
|
||||
className={cn(
|
||||
"h-6 w-6 flex-none transition group-hover:border-slate-750",
|
||||
trailingIconClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export type RenderIcon = IconNamesOrString | React.ComponentType<any>;
|
||||
|
||||
type IconProps = {
|
||||
icon?: RenderIcon;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** Use this icon to either render a passed in React component, or a NamedIcon/CompanyIcon */
|
||||
export function Icon(props: IconProps) {
|
||||
if (typeof props.icon === "string") {
|
||||
return <NamedIcon name={props.icon} className={props.className ?? ""} fallback={<></>} />;
|
||||
}
|
||||
|
||||
const Icon = props.icon;
|
||||
|
||||
if (!Icon) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <Icon className={props.className} />;
|
||||
}
|
||||
|
||||
export function IconInBox({ boxClassName, ...props }: IconProps & { boxClassName?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-9 w-9 place-content-center rounded-sm border border-slate-750 bg-slate-850",
|
||||
boxClassName
|
||||
)}
|
||||
>
|
||||
<Icon icon={props.icon} className={cn("h-6 w-6", props.className)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -131,6 +131,7 @@ const icons = {
|
||||
"clipboard-checked": (className: string) => (
|
||||
<ClipboardDocumentCheckIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
list: (className: string) => <ListBulletIcon className={cn("text-slate-400", className)} />,
|
||||
log: (className: string) => (
|
||||
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
const isMac = platform === "mac";
|
||||
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const modifiers = relevantShortcut.modifiers ?? [];
|
||||
const character = relevantShortcut.key;
|
||||
const character = keyString(relevantShortcut.key, isMac);
|
||||
|
||||
return (
|
||||
<span className={cn(variants[variant], className)}>
|
||||
@@ -35,6 +35,15 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
);
|
||||
}
|
||||
|
||||
function keyString(key: String, isMac: boolean) {
|
||||
switch (key) {
|
||||
case "enter":
|
||||
return isMac ? "↵" : key;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
function modifierString(modifier: Modifier, isMac: boolean) {
|
||||
switch (modifier) {
|
||||
case "alt":
|
||||
@@ -42,8 +51,10 @@ function modifierString(modifier: Modifier, isMac: boolean) {
|
||||
case "ctrl":
|
||||
return isMac ? "⌃" : "Ctrl+";
|
||||
case "meta":
|
||||
return isMac ? "⌘" : "⊞";
|
||||
return isMac ? "⌘" : "⊞+";
|
||||
case "shift":
|
||||
return isMac ? "⇧" : "Shift+";
|
||||
case "mod":
|
||||
return isMac ? "⌘" : "Ctrl+";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { useMemo } from "react";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { Run } from "~/presenters/RunPresenter.server";
|
||||
import { ViewRun } from "~/presenters/RunPresenter.server";
|
||||
import { cancelSchema } from "~/routes/resources.runs.$runId.cancel";
|
||||
import { schema } from "~/routes/resources.runs.$runId.rerun";
|
||||
import { formatDuration } from "~/utils";
|
||||
@@ -59,7 +59,7 @@ import { TaskCard } from "./TaskCard";
|
||||
import { TaskCardSkeleton } from "./TaskCardSkeleton";
|
||||
|
||||
type RunOverviewProps = {
|
||||
run: Run;
|
||||
run: ViewRun;
|
||||
trigger: {
|
||||
icon: string;
|
||||
title: string;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Fragment, useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Task } from "~/presenters/RunPresenter.server";
|
||||
import { ViewTask } from "~/presenters/RunPresenter.server";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from "./RunCard";
|
||||
import { TaskStatusIcon } from "./TaskStatus";
|
||||
|
||||
type TaskCardProps = Task & {
|
||||
type TaskCardProps = ViewTask & {
|
||||
selectedId?: string;
|
||||
selectedTask: (id: string) => void;
|
||||
isLast: boolean;
|
||||
|
||||
@@ -25,7 +25,7 @@ export function TriggerDetail({
|
||||
};
|
||||
properties: DisplayProperty[];
|
||||
}) {
|
||||
const { id, name, payload, timestamp, deliveredAt } = trigger;
|
||||
const { id, name, payload, context, timestamp, deliveredAt } = trigger;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
@@ -45,6 +45,7 @@ export function TriggerDetail({
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconProperty icon="id" label="Event name" value={name} />
|
||||
<RunPanelIconProperty icon="account" label="Event ID" value={id} />
|
||||
{trigger.externalAccount && (
|
||||
<RunPanelIconProperty
|
||||
icon="account"
|
||||
@@ -62,7 +63,9 @@ export function TriggerDetail({
|
||||
</div>
|
||||
)}
|
||||
<Header3>Payload</Header3>
|
||||
<CodeBlock code={JSON.stringify(payload, null, 2)} />
|
||||
<CodeBlock code={payload} />
|
||||
<Header3>Context</Header3>
|
||||
<CodeBlock code={context} />
|
||||
</div>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { DetailCell } from "../primitives/DetailCell";
|
||||
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
|
||||
import { DateTime, DateTimeAccurate } from "../primitives/DateTime";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/DetailCells",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Examples>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => <Examples />,
|
||||
};
|
||||
|
||||
function Examples() {
|
||||
return (
|
||||
<div className="flex max-w-xl flex-col items-start gap-y-8 p-8">
|
||||
<DetailCell
|
||||
leadingIcon="integration"
|
||||
leadingIconClassName="text-dimmed"
|
||||
label="Learn how to create your own API Integrations"
|
||||
variant="base"
|
||||
trailingIcon="external-link"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
<DetailCell
|
||||
leadingIcon={CodeBracketIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
label="Issue comment created"
|
||||
trailingIcon="check"
|
||||
trailingIconClassName="text-green-500 group-hover:text-green-400"
|
||||
/>
|
||||
<DetailCell
|
||||
leadingIcon={ClockIcon}
|
||||
leadingIconClassName="text-slate-400"
|
||||
label={<DateTime date={new Date()} />}
|
||||
description="Run #42 complete"
|
||||
trailingIcon="plus"
|
||||
trailingIconClassName="text-slate-500 group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,8 @@ const shortcuts: ShortcutDefinition[] = [
|
||||
{ key: "f", modifiers: ["meta"] },
|
||||
{ key: "k", modifiers: ["meta"] },
|
||||
{ key: "del", modifiers: ["ctrl", "alt"] },
|
||||
{ key: "enter", modifiers: ["meta"] },
|
||||
{ key: "enter", modifiers: ["mod"] },
|
||||
];
|
||||
|
||||
function Collection() {
|
||||
@@ -67,6 +69,9 @@ function Set({ platform }: { platform: "mac" | "windows" }) {
|
||||
<Button variant="danger/medium" shortcut={shortcut}>
|
||||
Danger medium
|
||||
</Button>
|
||||
<Button variant="danger/medium" shortcut={shortcut}>
|
||||
Danger medium
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</OperatingSystemContextProvider>
|
||||
|
||||
@@ -5,3 +5,4 @@ export const DEFAULT_MAX_CONCURRENT_RUNS = 10;
|
||||
export const MAX_CONCURRENT_RUNS_LIMIT = 20;
|
||||
export const PREPROCESS_RETRY_LIMIT = 2;
|
||||
export const EXECUTE_JOB_RETRY_LIMIT = 10;
|
||||
export const MAX_RUN_YIELDED_EXECUTIONS = 100;
|
||||
|
||||
@@ -31,7 +31,7 @@ export type PrismaTransactionOptions = {
|
||||
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
|
||||
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||
|
||||
rethrowPrismaErrors?: boolean;
|
||||
swallowPrismaErrors?: boolean;
|
||||
};
|
||||
|
||||
export async function $transaction<R>(
|
||||
@@ -55,11 +55,9 @@ export async function $transaction<R>(
|
||||
name: error.name,
|
||||
});
|
||||
|
||||
if (options?.rethrowPrismaErrors) {
|
||||
throw error;
|
||||
if (options?.swallowPrismaErrors) {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -124,6 +122,10 @@ function getClient() {
|
||||
emit: "stdout",
|
||||
level: "warn",
|
||||
},
|
||||
// {
|
||||
// emit: "stdout",
|
||||
// level: "query",
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider";
|
||||
|
||||
export type Modifier = "alt" | "ctrl" | "meta" | "shift";
|
||||
export type Modifier = "alt" | "ctrl" | "meta" | "shift" | "mod";
|
||||
|
||||
export type Shortcut = {
|
||||
key: string;
|
||||
modifiers?: Modifier[];
|
||||
enabledOnInputElements?: boolean;
|
||||
};
|
||||
|
||||
export type ShortcutDefinition =
|
||||
@@ -20,19 +20,31 @@ type useShortcutKeysProps = {
|
||||
shortcut: ShortcutDefinition;
|
||||
action: (event: KeyboardEvent) => void;
|
||||
disabled?: boolean;
|
||||
enabledOnInputElements?: boolean;
|
||||
};
|
||||
|
||||
export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) {
|
||||
const keys = createKeysFromShortcut(shortcut);
|
||||
useHotkeys(keys, action, { enabled: !disabled });
|
||||
}
|
||||
|
||||
function createKeysFromShortcut(shortcut: ShortcutDefinition) {
|
||||
const { platform } = useOperatingSystem();
|
||||
const isMac = platform === "mac";
|
||||
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const modifiers = relevantShortcut.modifiers;
|
||||
const character = relevantShortcut.key;
|
||||
const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
|
||||
return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character;
|
||||
const keys = createKeysFromShortcut(relevantShortcut);
|
||||
useHotkeys(
|
||||
keys,
|
||||
(event, hotkeysEvent) => {
|
||||
action(event);
|
||||
},
|
||||
{
|
||||
enabled: !disabled,
|
||||
enableOnFormTags: relevantShortcut.enabledOnInputElements,
|
||||
enableOnContentEditable: relevantShortcut.enabledOnInputElements,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createKeysFromShortcut(shortcut: Shortcut) {
|
||||
const modifiers = shortcut.modifiers;
|
||||
const character = shortcut.key;
|
||||
|
||||
return modifiers ? modifiers.map((k) => k).join("+") + "+" + character : character;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
import { customAlphabet } from "nanoid";
|
||||
import slug from "slug";
|
||||
import { prisma, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { createProject } from "./project.server";
|
||||
|
||||
export type { Organization };
|
||||
@@ -76,6 +75,10 @@ export async function createOrganization(
|
||||
},
|
||||
attemptCount = 0
|
||||
): Promise<Organization & { projects: Project[] }> {
|
||||
if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) {
|
||||
throw new Error("Organization could not be created.");
|
||||
}
|
||||
|
||||
const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`;
|
||||
|
||||
const orgWithSameSlug = await prisma.organization.findFirst({
|
||||
@@ -172,10 +175,10 @@ function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
return "prod";
|
||||
}
|
||||
case "STAGING": {
|
||||
return "staging";
|
||||
return "stg";
|
||||
}
|
||||
case "PREVIEW": {
|
||||
return "preview";
|
||||
return "prev";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function createProject(
|
||||
|
||||
// Create the dev and prod environments
|
||||
await createEnvironment(organization, project, "PRODUCTION");
|
||||
await createEnvironment(organization, project, "STAGING");
|
||||
|
||||
for (const member of project.organization.members) {
|
||||
await createEnvironment(organization, project, "DEVELOPMENT", member);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Task, TaskAttempt } from "@trigger.dev/database";
|
||||
import { ServerTask } from "@trigger.dev/core";
|
||||
import { CachedTask, ServerTask } from "@trigger.dev/core";
|
||||
|
||||
export type TaskWithAttempts = Task & { attempts: TaskAttempt[] };
|
||||
|
||||
@@ -23,5 +23,90 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
|
||||
attempts: task.attempts.length,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
operation: task.operation,
|
||||
callbackUrl: task.callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export type TaskForCaching = Pick<
|
||||
Task,
|
||||
"id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId"
|
||||
>;
|
||||
|
||||
export function prepareTasksForCaching(
|
||||
possibleTasks: TaskForCaching[],
|
||||
maxSize: number
|
||||
): {
|
||||
tasks: CachedTask[];
|
||||
cursor: string | undefined;
|
||||
} {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && !task.noop);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = maxSize;
|
||||
|
||||
for (const task of tasks) {
|
||||
const cachedTask = prepareTaskForCaching(task);
|
||||
const size = calculateCachedTaskSize(cachedTask);
|
||||
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(cachedTask);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: tasksToRun,
|
||||
cursor: tasks.length > tasksToRun.length ? tasks[tasksToRun.length].id : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareTasksForCachingLegacy(
|
||||
possibleTasks: TaskForCaching[],
|
||||
maxSize: number
|
||||
): {
|
||||
tasks: CachedTask[];
|
||||
cursor: string | undefined;
|
||||
} {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED");
|
||||
|
||||
// Prepare tasks and calculate their sizes
|
||||
const availableTasks = tasks.map((task) => {
|
||||
const cachedTask = prepareTaskForCaching(task);
|
||||
return { task: cachedTask, size: calculateCachedTaskSize(cachedTask) };
|
||||
});
|
||||
|
||||
// Sort tasks in ascending order by size
|
||||
availableTasks.sort((a, b) => a.size - b.size);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = maxSize;
|
||||
|
||||
for (const { task, size } of availableTasks) {
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(task);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: tasksToRun,
|
||||
cursor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function prepareTaskForCaching(task: TaskForCaching): CachedTask {
|
||||
return {
|
||||
id: task.idempotencyKey, // We should eventually move this back to task.id
|
||||
status: task.status,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
noop: task.noop,
|
||||
output: task.output as any,
|
||||
parentId: task.parentId,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateCachedTaskSize(task: CachedTask): number {
|
||||
return JSON.stringify(task).length;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Job } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
|
||||
type ApiRunOptions = {
|
||||
runId: Job["id"];
|
||||
maxTasks?: number;
|
||||
taskDetails?: boolean;
|
||||
subTasks?: boolean;
|
||||
cursor?: string;
|
||||
};
|
||||
|
||||
export class ApiRunPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
runId,
|
||||
maxTasks = 20,
|
||||
taskDetails = false,
|
||||
subTasks = false,
|
||||
cursor,
|
||||
}: ApiRunOptions) {
|
||||
const take = Math.min(maxTasks, 50);
|
||||
|
||||
return await prisma.jobRun.findUnique({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
startedAt: true,
|
||||
updatedAt: true,
|
||||
completedAt: true,
|
||||
environmentId: true,
|
||||
output: true,
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
displayKey: true,
|
||||
status: true,
|
||||
name: true,
|
||||
icon: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
params: taskDetails,
|
||||
output: taskDetails,
|
||||
},
|
||||
where: {
|
||||
parentId: subTasks ? undefined : null,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
take: take + 1,
|
||||
cursor: cursor
|
||||
? {
|
||||
id: cursor,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
statuses: {
|
||||
select: { key: true, label: true, state: true, data: true, history: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,19 @@ import { PrismaClient, prisma } from "~/db.server";
|
||||
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import {
|
||||
import type {
|
||||
Endpoint,
|
||||
EndpointIndex,
|
||||
RuntimeEnvironment,
|
||||
RuntimeEnvironmentType,
|
||||
} from "../../../../packages/database/src";
|
||||
import { env } from "~/env.server";
|
||||
} from "@trigger.dev/database";
|
||||
|
||||
export type Client = {
|
||||
slug: string;
|
||||
endpoints: {
|
||||
DEVELOPMENT: ClientEndpoint;
|
||||
PRODUCTION: ClientEndpoint;
|
||||
STAGING?: ClientEndpoint;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -133,6 +133,8 @@ export class EnvironmentsPresenter {
|
||||
throw new Error("Development environment not found, this should not happen");
|
||||
}
|
||||
|
||||
const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING");
|
||||
|
||||
const productionEnvironment = filtered.find(
|
||||
(environment) => environment.type === "PRODUCTION"
|
||||
);
|
||||
@@ -151,6 +153,9 @@ export class EnvironmentsPresenter {
|
||||
state: "unconfigured",
|
||||
environment: productionEnvironment,
|
||||
},
|
||||
STAGING: stagingEnvironment
|
||||
? { state: "unconfigured", environment: stagingEnvironment }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -161,6 +166,16 @@ export class EnvironmentsPresenter {
|
||||
client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl);
|
||||
}
|
||||
|
||||
if (stagingEnvironment) {
|
||||
const stagingEndpoint = stagingEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
|
||||
if (stagingEndpoint) {
|
||||
client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const prodEndpoint = productionEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
|
||||
export class OrgUsagePresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({ userId, slug }: { userId: string; slug: string }) {
|
||||
const organization = await this.#prismaClient.organization.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
|
||||
const startOfLastMonth = new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1); // this works for January as well
|
||||
|
||||
// Get count of runs since the start of the current month
|
||||
const runsCount = await this.#prismaClient.jobRun.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get the count of runs for last month
|
||||
const runsCountLastMonth = await this.#prismaClient.jobRun.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
gte: startOfLastMonth,
|
||||
lt: startOfMonth,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get the count of the runs for the last 6 months, by month. So for example we want the data shape to be:
|
||||
// [
|
||||
// { month: "2021-01", count: 10 },
|
||||
// { month: "2021-02", count: 20 },
|
||||
// { month: "2021-03", count: 30 },
|
||||
// { month: "2021-04", count: 40 },
|
||||
// { month: "2021-05", count: 50 },
|
||||
// { month: "2021-06", count: 60 },
|
||||
// ]
|
||||
// This will be used to generate the chart on the usage page
|
||||
// Use prisma queryRaw for this since prisma doesn't support grouping by month
|
||||
const chartDataRaw = await this.#prismaClient.$queryRaw<
|
||||
{
|
||||
month: string;
|
||||
count: number;
|
||||
}[]
|
||||
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' GROUP BY month ORDER BY month ASC`;
|
||||
|
||||
const chartData = chartDataRaw.map((obj) => ({
|
||||
name: obj.month,
|
||||
total: Number(obj.count), // Convert BigInt to Number
|
||||
}));
|
||||
|
||||
const totalJobs = await this.#prismaClient.job.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
const totalJobsLastMonth = await this.#prismaClient.job.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
lt: startOfMonth,
|
||||
},
|
||||
deletedAt: null,
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
const totalIntegrations = await this.#prismaClient.integration.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const totalIntegrationsLastMonth = await this.#prismaClient.integration.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
createdAt: {
|
||||
lt: startOfMonth,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const totalMembers = await this.#prismaClient.orgMember.count({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const jobs = await this.#prismaClient.job.findMany({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
deletedAt: null,
|
||||
internal: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
_count: {
|
||||
select: {
|
||||
runs: {
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startOfMonth,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: organization.id,
|
||||
runsCount,
|
||||
runsCountLastMonth,
|
||||
chartData: fillInMissingMonthlyData(chartData, 6),
|
||||
totalJobs,
|
||||
totalJobsLastMonth,
|
||||
totalIntegrations,
|
||||
totalIntegrationsLastMonth,
|
||||
totalMembers,
|
||||
jobs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// This will fill in missing chart data with zeros
|
||||
// So for example, if data is [{ name: "2021-01", total: 10 }, { name: "2021-03", total: 30 }] and the totalNumberOfMonths is 6
|
||||
// And the current month is "2021-04", then this function will return:
|
||||
// [{ name: "2020-11", total: 0 }, { name: "2020-12", total: 0 }, { name: "2021-01", total: 10 }, { name: "2021-02", total: 0 }, { name: "2021-03", total: 30 }, { name: "2021-04", total: 0 }]
|
||||
function fillInMissingMonthlyData(
|
||||
data: Array<{ name: string; total: number }>,
|
||||
totalNumberOfMonths: number
|
||||
): Array<{ name: string; total: number }> {
|
||||
const currentMonth = new Date().toISOString().slice(0, 7);
|
||||
|
||||
const startMonth = new Date(
|
||||
new Date(currentMonth).getFullYear(),
|
||||
new Date(currentMonth).getMonth() - totalNumberOfMonths,
|
||||
1
|
||||
)
|
||||
.toISOString()
|
||||
.slice(0, 7);
|
||||
|
||||
const months = getMonthsBetween(startMonth, currentMonth);
|
||||
|
||||
let completeData = months.map((month) => {
|
||||
let foundData = data.find((d) => d.name === month);
|
||||
return foundData ? { ...foundData } : { name: month, total: 0 };
|
||||
});
|
||||
|
||||
return completeData;
|
||||
}
|
||||
|
||||
function getMonthsBetween(startMonth: string, endMonth: string): string[] {
|
||||
const startDate = new Date(startMonth);
|
||||
const endDate = new Date(endMonth);
|
||||
|
||||
const months = [];
|
||||
let currentDate = startDate;
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
months.push(currentDate.toISOString().slice(0, 7));
|
||||
currentDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1));
|
||||
}
|
||||
|
||||
return months;
|
||||
}
|
||||
@@ -13,10 +13,11 @@ type RunOptions = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type Run = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>;
|
||||
export type Task = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["tasks"][number];
|
||||
export type Event = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["event"];
|
||||
export type ViewRun = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>;
|
||||
export type ViewTask = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["tasks"][number];
|
||||
export type ViewEvent = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["event"];
|
||||
|
||||
type QueryEvent = NonNullable<Awaited<ReturnType<RunPresenter["query"]>>>["event"];
|
||||
type QueryTask = NonNullable<Awaited<ReturnType<RunPresenter["query"]>>>["tasks"][number];
|
||||
|
||||
export class RunPresenter {
|
||||
@@ -76,7 +77,7 @@ export class RunPresenter {
|
||||
type: run.environment.type,
|
||||
slug: run.environment.slug,
|
||||
},
|
||||
event: run.event,
|
||||
event: this.#prepareEventData(run.event),
|
||||
tasks,
|
||||
runConnections: run.runConnections,
|
||||
missingConnections: run.missingConnections,
|
||||
@@ -84,6 +85,22 @@ export class RunPresenter {
|
||||
};
|
||||
}
|
||||
|
||||
#prepareEventData(event: QueryEvent) {
|
||||
return {
|
||||
id: event.eventId,
|
||||
name: event.name,
|
||||
payload: JSON.stringify(event.payload),
|
||||
context: JSON.stringify(event.context),
|
||||
timestamp: event.timestamp,
|
||||
deliveredAt: event.deliveredAt,
|
||||
externalAccount: event.externalAccount
|
||||
? {
|
||||
identifier: event.externalAccount.identifier,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
query({ id, userId }: RunOptions) {
|
||||
return this.#prismaClient.jobRun.findFirst({
|
||||
select: {
|
||||
@@ -110,9 +127,10 @@ export class RunPresenter {
|
||||
},
|
||||
event: {
|
||||
select: {
|
||||
id: true,
|
||||
eventId: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
context: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { RedactSchema } from "@trigger.dev/core";
|
||||
import { StyleSchema } from "@trigger.dev/core";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
import { Redactor } from "~/utils/redactor";
|
||||
|
||||
type DetailsProps = {
|
||||
id: string;
|
||||
@@ -61,6 +63,7 @@ export class TaskDetailsPresenter {
|
||||
completedAt: true,
|
||||
style: true,
|
||||
parentId: true,
|
||||
redact: true,
|
||||
attempts: {
|
||||
select: {
|
||||
number: true,
|
||||
@@ -85,11 +88,32 @@ export class TaskDetailsPresenter {
|
||||
|
||||
return {
|
||||
...task,
|
||||
output: task.output ? JSON.stringify(task.output, null, 2) : undefined,
|
||||
redact: undefined,
|
||||
output: task.output
|
||||
? JSON.stringify(this.#stringifyOutputWithRedactions(task.output, task.redact), null, 2)
|
||||
: undefined,
|
||||
connection: task.runConnection,
|
||||
params: task.params as Record<string, any>,
|
||||
properties: mergeProperties(task.properties, task.outputProperties),
|
||||
style: task.style ? StyleSchema.parse(task.style) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
#stringifyOutputWithRedactions(output: any, redact: unknown): any {
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedRedact = RedactSchema.safeParse(redact);
|
||||
|
||||
if (!parsedRedact.success) {
|
||||
return output;
|
||||
}
|
||||
|
||||
const paths = parsedRedact.data.paths;
|
||||
|
||||
const redactor = new Redactor(paths);
|
||||
|
||||
return redactor.redact(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Job } from "~/models/job.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { EventExample } from "@trigger.dev/core";
|
||||
|
||||
export class TestJobPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -67,14 +68,22 @@ export class TestJobPresenter {
|
||||
name: "latest",
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
runs: {
|
||||
select: {
|
||||
runs: {
|
||||
where: {
|
||||
isTest: true,
|
||||
id: true,
|
||||
createdAt: true,
|
||||
number: true,
|
||||
status: true,
|
||||
event: {
|
||||
select: {
|
||||
payload: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
@@ -97,6 +106,15 @@ export class TestJobPresenter {
|
||||
throw new Error("Job not found");
|
||||
}
|
||||
|
||||
//collect together the examples, we don't care about the environments
|
||||
const examples = job.aliases.flatMap((alias) =>
|
||||
alias.version.examples.map((example) => ({
|
||||
...example,
|
||||
icon: example.icon ?? undefined,
|
||||
payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
environments: job.aliases.map((alias) => ({
|
||||
id: alias.environment.id,
|
||||
@@ -104,15 +122,18 @@ export class TestJobPresenter {
|
||||
slug: alias.environment.slug,
|
||||
userId: alias.environment.orgMember?.userId,
|
||||
versionId: alias.version.id,
|
||||
examples: alias.version.examples.map((example) => ({
|
||||
...example,
|
||||
payload: JSON.stringify(example.payload, exampleReplacer, 2),
|
||||
})),
|
||||
hasAuthResolver: alias.version.integrations.some(
|
||||
(i) => i.integration.authSource === "RESOLVER"
|
||||
),
|
||||
})),
|
||||
hasTestRuns: job._count.runs > 0,
|
||||
examples,
|
||||
runs: job.runs.map((r) => ({
|
||||
id: r.id,
|
||||
number: r.number,
|
||||
status: r.status,
|
||||
created: r.createdAt,
|
||||
payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ export class TriggerDetailsPresenter {
|
||||
select: {
|
||||
event: {
|
||||
select: {
|
||||
id: true,
|
||||
eventId: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
context: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
@@ -32,6 +33,18 @@ export class TriggerDetailsPresenter {
|
||||
},
|
||||
});
|
||||
|
||||
return event;
|
||||
return {
|
||||
id: event.eventId,
|
||||
name: event.name,
|
||||
payload: JSON.stringify(event.payload, null, 2),
|
||||
context: JSON.stringify(event.context, null, 2),
|
||||
timestamp: event.timestamp,
|
||||
deliveredAt: event.deliveredAt,
|
||||
externalAccount: event.externalAccount
|
||||
? {
|
||||
identifier: event.externalAccount.identifier,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,178 @@
|
||||
import { ComingSoon } from "~/components/ComingSoon";
|
||||
import { PageContainer, PageBody } from "~/components/layout/AppLayout";
|
||||
import { ArrowRightIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
ForwardIcon,
|
||||
SquaresPlusIcon,
|
||||
UsersIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder";
|
||||
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
|
||||
import { Link } from "@remix-run/react/dist/components";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ params, request }: LoaderArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug } = OrganizationParamsSchema.parse(params);
|
||||
|
||||
const presenter = new OrgUsagePresenter();
|
||||
|
||||
const data = await presenter.call({ userId, slug: organizationSlug });
|
||||
|
||||
if (!data) {
|
||||
throw new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
return typedjson(data);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
|
||||
if (active && payload) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded border border-border bg-slate-900 px-4 py-2 text-sm text-dimmed">
|
||||
<p className="text-white">{label}:</p>
|
||||
<p className="text-white">{payload[0].value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const loaderData = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<OrgAdminHeader />
|
||||
<PageBody>
|
||||
<ComingSoon
|
||||
title="Usage & billing"
|
||||
description="View your usage, tier and billing information. During the beta we will display usage and start billing if you exceed your limits. But don't worry, we'll give you plenty of warning."
|
||||
icon="billing"
|
||||
/>
|
||||
<div className="mb-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Total Runs this month</Header2>
|
||||
<ForwardIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.runsCount.toLocaleString()}</p>
|
||||
<Paragraph variant="small" className="text-dimmed">
|
||||
{loaderData.runsCountLastMonth} runs last month
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Total Jobs</Header2>
|
||||
<WrenchScrewdriverIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.totalJobs.toLocaleString()}</p>
|
||||
<Paragraph variant="small" className="text-dimmed">
|
||||
{loaderData.totalJobs === loaderData.totalJobsLastMonth ? (
|
||||
<>No change since last month</>
|
||||
) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? (
|
||||
<>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month</>
|
||||
) : (
|
||||
<>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month</>
|
||||
)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Total Integrations</Header2>
|
||||
<SquaresPlusIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.totalIntegrations.toLocaleString()}</p>
|
||||
<Paragraph variant="small" className="text-dimmed">
|
||||
{loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? (
|
||||
<>No change since last month</>
|
||||
) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? (
|
||||
<>
|
||||
+{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since
|
||||
last month
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
-{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since
|
||||
last month
|
||||
</>
|
||||
)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border border-border p-6">
|
||||
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Header2>Team members</Header2>
|
||||
<UsersIcon className="h-6 w-6 text-dimmed" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{loaderData.totalMembers.toLocaleString()}</p>
|
||||
<TextLink
|
||||
to={organizationTeamPath(organization)}
|
||||
className="group text-sm text-dimmed hover:text-bright"
|
||||
>
|
||||
Manage
|
||||
<ArrowRightIcon className="-mb-0.5 ml-0.5 h-4 w-4 text-dimmed transition group-hover:translate-x-1 group-hover:text-bright" />
|
||||
</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex max-h-[500px] gap-x-4">
|
||||
<div className="w-1/2 rounded border border-border py-6 pr-2">
|
||||
<Header2 className="mb-8 pl-6">Job Runs per month</Header2>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<BarChart data={loaderData.chartData}>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#888888"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#888888"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => `${value}`}
|
||||
/>
|
||||
<Tooltip cursor={{ fill: "rgba(255,255,255,0.05)" }} content={<CustomTooltip />} />
|
||||
<Bar dataKey="total" fill="#DB2777" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="w-1/2 overflow-y-auto rounded border border-border px-3 py-6">
|
||||
<div className="mb-2 flex items-baseline justify-between border-b border-border px-3 pb-4">
|
||||
<Header2 className="">Jobs</Header2>
|
||||
<Header2 className="">Runs</Header2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{loaderData.jobs.map((job) => (
|
||||
<Link
|
||||
to={jobPath(organization, job.project, job)}
|
||||
className="flex items-center rounded px-4 py-3 transition hover:bg-slate-850"
|
||||
key={job.id}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{job.slug}</p>
|
||||
<p className="text-sm text-muted-foreground">Project: {job.project.name}</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">{job._count.runs.toLocaleString()}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
+2
-1
@@ -104,6 +104,7 @@ export default function Page() {
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<HelpTrigger title="Example Jobs and inspiration" />
|
||||
</div>
|
||||
@@ -160,7 +161,7 @@ function ExampleJobs() {
|
||||
height="250"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
className="mb-4 w-full border-b border-slate-800"
|
||||
className="mb-4 border-b border-slate-800"
|
||||
/>
|
||||
<Header2 spacing>How to create a Job</Header2>
|
||||
<Paragraph variant="small" spacing>
|
||||
|
||||
+15
-3
@@ -85,8 +85,8 @@ export default function Page() {
|
||||
const client = clients.find((c) => c.slug === selected.client);
|
||||
if (!client) return undefined;
|
||||
|
||||
if (selected.type === "PREVIEW" || selected.type === "STAGING") {
|
||||
throw new Error("PREVIEW/STAGING is not yet supported");
|
||||
if (selected.type === "PREVIEW") {
|
||||
throw new Error("PREVIEW is not yet supported");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -195,6 +195,18 @@ export default function Page() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
{client.endpoints.STAGING && (
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.STAGING}
|
||||
type="STAGING"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "STAGING",
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.PRODUCTION}
|
||||
type="PRODUCTION"
|
||||
@@ -218,7 +230,7 @@ export default function Page() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedEndpoint && (
|
||||
{selectedEndpoint && selectedEndpoint.endpoint && (
|
||||
<ConfigureEndpointSheet
|
||||
slug={selectedEndpoint.clientSlug}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
|
||||
+16
-72
@@ -13,6 +13,7 @@ import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { DetailCell } from "~/components/primitives/DetailCell";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
@@ -209,10 +210,12 @@ function PossibleIntegrationsList({
|
||||
<Feedback
|
||||
button={
|
||||
<button className="w-full">
|
||||
<ExternalIntegrationLink
|
||||
name="plus"
|
||||
<DetailCell
|
||||
leadingIcon="plus"
|
||||
leadingIconClassName="text-dimmed"
|
||||
label="Request an API and we'll add it to the list as an Integration"
|
||||
trailingIcon="chevron-right"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
@@ -221,10 +224,12 @@ function PossibleIntegrationsList({
|
||||
|
||||
<Header2 className="mb-2 mt-6">Create an Integration</Header2>
|
||||
<a href="https://docs.trigger.dev/integrations/create" target="_blank">
|
||||
<ExternalIntegrationLink
|
||||
name="integration"
|
||||
<DetailCell
|
||||
leadingIcon="integration"
|
||||
leadingIconClassName="text-dimmed"
|
||||
label="Learn how to create your own API Integrations"
|
||||
trailingIcon="external-link"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
@@ -482,77 +487,16 @@ function AddIntegrationConnection({
|
||||
icon?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-900">
|
||||
<NamedIconInBox
|
||||
name={icon ?? identifier}
|
||||
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
|
||||
/>
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className="m-0 flex-1 text-left leading-[1.1rem] transition group-hover:text-bright"
|
||||
>
|
||||
{name}
|
||||
</Paragraph>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
{isIntegration && <IntegrationIcon />}
|
||||
<NamedIcon
|
||||
name="plus"
|
||||
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExternalIntegrationLink({
|
||||
name,
|
||||
label,
|
||||
trailingIcon,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
trailingIcon: string;
|
||||
}) {
|
||||
return (
|
||||
<span className="group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-850">
|
||||
<NamedIconInBox
|
||||
name={name}
|
||||
className="h-9 w-9 flex-none text-dimmed transition group-hover:border-slate-750"
|
||||
iconClassName="text-dimmed"
|
||||
/>
|
||||
<Paragraph variant="base" className="m-0 flex-1 text-left transition group-hover:text-bright">
|
||||
{label}
|
||||
</Paragraph>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<NamedIcon
|
||||
name={trailingIcon}
|
||||
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<DetailCell
|
||||
className="w-full"
|
||||
leadingIcon={icon ?? identifier}
|
||||
label={name}
|
||||
trailingIcon="plus"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntegrationIcon() {
|
||||
return <LogoIcon className="h-3.5 w-3.5 flex-none pb-0.5" />;
|
||||
}
|
||||
|
||||
function InfoLink({ text }: { text: string }) {
|
||||
return (
|
||||
<div className="group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-850">
|
||||
<NamedIconInBox
|
||||
name="integration"
|
||||
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
|
||||
/>
|
||||
<Paragraph variant="base" className="m-0 flex-1 text-left transition group-hover:text-bright">
|
||||
{text}
|
||||
</Paragraph>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<NamedIcon
|
||||
name="docs"
|
||||
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+195
-126
@@ -1,6 +1,7 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { PopoverTrigger } from "@radix-ui/react-popover";
|
||||
import { ClipboardIcon } from "@heroicons/react/20/solid";
|
||||
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
|
||||
import { Form, useActionData, useSubmit } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
@@ -8,16 +9,16 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { JSONEditor } from "~/components/code/JSONEditor";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { HowToRunATest } from "~/components/helpContent/HelpContentText";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { DetailCell } from "~/components/primitives/DetailCell";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Hint } from "~/components/primitives/Hint";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Popover, PopoverContent } from "~/components/primitives/Popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -26,6 +27,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses";
|
||||
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { TestJobPresenter } from "~/presenters/TestJobPresenter.server";
|
||||
import { TestJobService } from "~/services/jobs/testJob.server";
|
||||
@@ -39,14 +42,14 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const { organizationSlug, projectParam, jobParam } = JobParamsSchema.parse(params);
|
||||
|
||||
const presenter = new TestJobPresenter();
|
||||
const { environments, hasTestRuns } = await presenter.call({
|
||||
const { environments, runs, examples } = await presenter.call({
|
||||
userId,
|
||||
organizationSlug,
|
||||
projectSlug: projectParam,
|
||||
jobSlug: jobParam,
|
||||
});
|
||||
|
||||
return typedjson({ environments, hasTestRuns });
|
||||
return typedjson({ environments, runs, examples });
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
@@ -116,22 +119,30 @@ export const handle: Handle = {
|
||||
const startingJson = "{\n\n}";
|
||||
|
||||
export default function Page() {
|
||||
const { environments, runs, examples } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
//form submission
|
||||
const submit = useSubmit();
|
||||
const lastSubmission = useActionData();
|
||||
const [isExamplePopoverOpen, setIsExamplePopoverOpen] = useState(false);
|
||||
const { environments, hasTestRuns } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
const [defaultJson, setDefaultJson] = useState<string>(startingJson);
|
||||
const currentJson = useRef<string>(defaultJson);
|
||||
//examples
|
||||
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(
|
||||
examples.at(0)?.id ?? runs.at(0)?.id
|
||||
);
|
||||
const selectedCodeSample =
|
||||
examples.find((e) => e.id === selectedCodeSampleId)?.payload ??
|
||||
runs.find((r) => r.id === selectedCodeSampleId)?.payload;
|
||||
|
||||
const [defaultJson, setDefaultJson] = useState<string>(selectedCodeSample ?? startingJson);
|
||||
const setCode = useCallback((code: string) => {
|
||||
setDefaultJson(code);
|
||||
}, []);
|
||||
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>(environments[0].id);
|
||||
const [currentAccountId, setCurrentAccountId] = useState<string | undefined>(undefined);
|
||||
|
||||
const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId);
|
||||
|
||||
const insertCode = useCallback((code: string) => {
|
||||
setDefaultJson(code);
|
||||
setIsExamplePopoverOpen(false);
|
||||
}, []);
|
||||
const currentJson = useRef<string>(defaultJson);
|
||||
const [currentAccountId, setCurrentAccountId] = useState<string | undefined>(undefined);
|
||||
|
||||
const submitForm = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -170,120 +181,178 @@ export default function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Help defaultOpen={true}>
|
||||
{(open) => (
|
||||
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
|
||||
<div className="flex h-fit max-h-full overflow-hidden">
|
||||
<Form
|
||||
className="flex max-h-full grow flex-col gap-2 overflow-y-auto"
|
||||
method="post"
|
||||
{...form.props}
|
||||
onSubmit={(e) => submitForm(e)}
|
||||
>
|
||||
<div className="flex flex-none items-center justify-between gap-2">
|
||||
<div className="flex flex-none items-center gap-2">
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={selectedEnvironmentId}
|
||||
onValueChange={setSelectedEnvironmentId}
|
||||
>
|
||||
<SelectTrigger size="secondary/small">
|
||||
<SelectValue placeholder="Select environment" className="m-0 p-0" />{" "}
|
||||
Environment
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{environments.map((environment) => (
|
||||
<SelectItem key={environment.id} value={environment.id}>
|
||||
<EnvironmentLabel environment={environment} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<div className="grid h-full grid-cols-1 gap-4">
|
||||
<div className="flex h-full max-h-full overflow-hidden">
|
||||
<Form
|
||||
className="flex h-full max-h-full grow flex-col gap-4 overflow-y-auto"
|
||||
method="post"
|
||||
{...form.props}
|
||||
onSubmit={(e) => submitForm(e)}
|
||||
>
|
||||
<div className="grid h-full grid-cols-[1fr_auto] overflow-hidden">
|
||||
<div className="relative h-full flex-1 overflow-hidden rounded-l border border-border">
|
||||
<JSONEditor
|
||||
defaultValue={defaultJson}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
onChange={(v) => {
|
||||
currentJson.current = v;
|
||||
|
||||
{selectedEnvironment && selectedEnvironment.examples.length > 0 && (
|
||||
<Popover
|
||||
open={isExamplePopoverOpen}
|
||||
onOpenChange={(open) => setIsExamplePopoverOpen(open)}
|
||||
//deselect the example if it's been edited
|
||||
if (selectedCodeSampleId) {
|
||||
if (v !== selectedCodeSample) {
|
||||
setDefaultJson(v);
|
||||
setSelectedCodeSampleId(undefined);
|
||||
}
|
||||
}
|
||||
}}
|
||||
height="100%"
|
||||
min-height="100%"
|
||||
max-height="100%"
|
||||
autoFocus
|
||||
placeholder="Use your schema to enter valid JSON or add one of the example payloads then click 'Run test'"
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full w-fit min-w-[20rem] flex-col gap-4 overflow-y-auto rounded-r border border-l-0 border-border p-4">
|
||||
{examples.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header2>Example payloads</Header2>
|
||||
{examples.map((example) => (
|
||||
<button
|
||||
type="button"
|
||||
key={example.id}
|
||||
onClick={(e) => {
|
||||
setCode(example.payload ?? "");
|
||||
setSelectedCodeSampleId(example.id);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<ButtonContent
|
||||
variant="secondary/small"
|
||||
LeadingIcon="beaker"
|
||||
TrailingIcon="chevron-down"
|
||||
>
|
||||
Insert an example
|
||||
</ButtonContent>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-80 p-0" align="start">
|
||||
{selectedEnvironment?.examples.map((example) => (
|
||||
<Button
|
||||
key={example.id}
|
||||
variant="menu-item"
|
||||
onClick={(e) => insertCode(example.payload)}
|
||||
LeadingIcon={example.icon ?? "beaker"}
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
{example.name}
|
||||
</Button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
<DetailCell
|
||||
leadingIcon={example.icon ?? CodeBracketIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
label={example.name}
|
||||
trailingIcon={example.id === selectedCodeSampleId ? "check" : "plus"}
|
||||
trailingIconClassName={
|
||||
example.id === selectedCodeSampleId
|
||||
? "text-green-500 group-hover:text-green-400"
|
||||
: "text-slate-500 group-hover:text-bright"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<HelpTrigger title="How do I run a test?" />
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header2>Recent payloads</Header2>
|
||||
{runs.length === 0 ? (
|
||||
<Callout variant="info">
|
||||
Recent payloads will show here once you've completed a Run.
|
||||
</Callout>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{runs.map((run) => (
|
||||
<button
|
||||
key={run.id}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
setCode(run.payload ?? "");
|
||||
setSelectedCodeSampleId(run.id);
|
||||
}}
|
||||
>
|
||||
<DetailCell
|
||||
leadingIcon={ClockIcon}
|
||||
leadingIconClassName="text-slate-400"
|
||||
label={<DateTime date={run.created} />}
|
||||
description={
|
||||
<>
|
||||
Run #{run.number}{" "}
|
||||
<span className={runStatusClassNameColor(run.status)}>
|
||||
{runStatusTitle(run.status).toLocaleLowerCase()}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
trailingIcon={run.id === selectedCodeSampleId ? "check" : "plus"}
|
||||
trailingIconClassName={
|
||||
run.id === selectedCodeSampleId
|
||||
? "text-green-500 group-hover:text-green-400"
|
||||
: "text-slate-500 group-hover:text-bright"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Payload</Label>
|
||||
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<JSONEditor
|
||||
defaultValue={defaultJson}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
onChange={(v) => (currentJson.current = v)}
|
||||
minHeight="150px"
|
||||
/>
|
||||
</div>
|
||||
</InputGroup>
|
||||
|
||||
{selectedEnvironment?.hasAuthResolver && (
|
||||
<InputGroup fullWidth className="mb-4 mt-4">
|
||||
<Label variant="small">Account ID</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
value={currentAccountId}
|
||||
placeholder={`e.g. abc_1234`}
|
||||
onChange={(e) => setCurrentAccountId(e.target.value)}
|
||||
/>
|
||||
<FormError>{accountId.error}</FormError>
|
||||
</InputGroup>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header2>Account ID</Header2>
|
||||
<InputGroup fullWidth>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="large"
|
||||
value={currentAccountId}
|
||||
placeholder={`e.g. abc_1234`}
|
||||
onChange={(e) => setCurrentAccountId(e.target.value)}
|
||||
/>
|
||||
<FormError>{accountId.error}</FormError>
|
||||
<Hint>
|
||||
Learn about testing Jobs with an Account ID in our{" "}
|
||||
<TextLink href="https://trigger.dev/docs/documentation/guides/using-integrations-byo-auth#testing-jobs-with-account-id">
|
||||
BYOAuth docs
|
||||
</TextLink>
|
||||
</Hint>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-none items-center justify-between">
|
||||
{payload.error ? (
|
||||
<FormError id={payload.errorId}>{payload.error}</FormError>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
LeadingIcon="beaker"
|
||||
leadingIconClassName="text-bright"
|
||||
>
|
||||
Run test
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
<HelpContent title="How to run a test" className="h-fit">
|
||||
<HowToRunATest />
|
||||
</HelpContent>
|
||||
</div>
|
||||
)}
|
||||
</Help>
|
||||
<div className="flex items-center justify-between">
|
||||
<LinkButton
|
||||
variant="tertiary/medium"
|
||||
to="https://trigger.dev/docs/documentation/guides/testing-jobs"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Learn more about running tests
|
||||
</LinkButton>
|
||||
<div className="flex flex-none items-center justify-end gap-2">
|
||||
{payload.error ? (
|
||||
<FormError id={payload.errorId}>{payload.error}</FormError>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={selectedEnvironmentId}
|
||||
onValueChange={setSelectedEnvironmentId}
|
||||
>
|
||||
<SelectTrigger size="medium">
|
||||
<SelectValue placeholder="Select environment" className="m-0 p-0" /> Environment
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{environments.map((environment) => (
|
||||
<SelectItem key={environment.id} value={environment.id}>
|
||||
<EnvironmentLabel environment={environment} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
LeadingIcon="beaker"
|
||||
leadingIconClassName="text-bright"
|
||||
shortcut={{ key: "enter", modifiers: ["mod"], enabledOnInputElements: true }}
|
||||
>
|
||||
Run test
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+14
-21
@@ -39,9 +39,14 @@ export default function SetUpAstro() {
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<AstroLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
@@ -76,28 +81,16 @@ export default function SetUpAstro() {
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Follow the steps from the Astro manual installation guide"
|
||||
title="Run the CLI 'init' command in an existing Astro project"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/astro"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
<div className="flex items-start justify-start gap-2"></div>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple
|
||||
example Job in <InlineCode variant="extra-small">example.ts</InlineCode> to help you
|
||||
get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Astro app" />
|
||||
<StepContentContainer>
|
||||
|
||||
+108
-9
@@ -1,21 +1,120 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Express" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="Express"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/451"
|
||||
githubIssueNumber={451}
|
||||
>
|
||||
<ExpressLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<ExpressLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Callout
|
||||
variant={"info"}
|
||||
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
|
||||
className="mb-8"
|
||||
>
|
||||
Trigger.dev has full support for serverless. We will be adding support for long-running
|
||||
servers soon.
|
||||
</Callout>
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Manually set up Trigger.dev in your existing Express project"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/express"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Express app" />
|
||||
<StepContentContainer>
|
||||
<RunDevCommand />
|
||||
<Callout variant="info">
|
||||
You may be using the `start` script instead, in which case substitute `dev` in the
|
||||
above commands.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
+211
-12
@@ -1,21 +1,220 @@
|
||||
import { NestjsLogo } from "~/assets/logos/NestjsLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { InstallPackages } from "~/components/code/InstallPackages";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { CodeBlock } from "../../components/code/CodeBlock";
|
||||
import { TriggerDevStep } from "~/components/SetupCommands";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Nest.js" />,
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="NestJS" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const AppModuleCode = `
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TriggerDevModule } from '@trigger.dev/nestjs';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TriggerDevModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
id: 'my-nest-app',
|
||||
apiKey: config.getOrThrow('TRIGGER_API_KEY'),
|
||||
apiUrl: config.getOrThrow('TRIGGER_API_URL'),
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
`;
|
||||
|
||||
const JobControllerCode = `
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { InjectTriggerDevClient } from '@trigger.dev/nestjs';
|
||||
import { eventTrigger, TriggerClient } from '@trigger.dev/sdk';
|
||||
|
||||
@Controller()
|
||||
export class JobController {
|
||||
constructor(
|
||||
@InjectTriggerDevClient() private readonly client: TriggerClient,
|
||||
) {
|
||||
this.client.defineJob({
|
||||
id: 'test-job',
|
||||
name: 'Test Job One',
|
||||
version: '0.0.1',
|
||||
trigger: eventTrigger({
|
||||
name: 'test.event',
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info('Hello world!', { payload });
|
||||
|
||||
return {
|
||||
message: 'Hello world!',
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return \`Running Trigger.dev with client-id \${this.client.id}\`;
|
||||
}
|
||||
}`;
|
||||
|
||||
const AppModuleWithControllerCode = `
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TriggerDevModule } from '@trigger.dev/nestjs';
|
||||
import { JobController } from './job.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TriggerDevModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
id: 'my-nest-app',
|
||||
apiKey: config.getOrThrow('TRIGGER_API_KEY'),
|
||||
apiUrl: config.getOrThrow('TRIGGER_API_URL'),
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [
|
||||
//...existingControllers,
|
||||
JobController
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
`;
|
||||
|
||||
const packageJsonCode = `"trigger.dev": {
|
||||
"endpointId": "my-nest-app"
|
||||
}`;
|
||||
|
||||
export default function SetupNestJS() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
invariant(devEnvironment, "devEnvironment is required");
|
||||
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="Nest.js"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/449"
|
||||
githubIssueNumber={449}
|
||||
>
|
||||
<NestjsLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 2 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Add the dependencies" />
|
||||
<StepContentContainer>
|
||||
<InstallPackages
|
||||
packages={["@trigger.dev/sdk", "@trigger.dev/nestjs", "@nestjs/config"]}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Add the environment variables" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Inside your <InlineCode>.env</InlineCode> file, create the following env variables:
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
fileName=".env"
|
||||
showChrome
|
||||
code={`TRIGGER_API_KEY=${devEnvironment.apiKey}\nTRIGGER_API_URL=${appOrigin}`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Add the TriggerDevModule" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Now, go to your <InlineCode>app.module.ts</InlineCode> and add the{" "}
|
||||
<InlineCode>TriggerDevModule</InlineCode>:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="app.module.ts" showChrome code={AppModuleCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Add the first job" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Create a <InlineCode>controller</InlineCode> called{" "}
|
||||
<InlineCode>job.controller.ts</InlineCode> and add the following code:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="src/job.controller.ts" showChrome code={JobControllerCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="5" title="Update your app.module.ts" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Now, add the new <InlineCode>controller</InlineCode> to your{" "}
|
||||
<InlineCode>app.module.ts</InlineCode>:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="app.module.ts" showChrome code={AppModuleWithControllerCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Update your package.json" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Now, add this to the top-level of your <InlineCode>package.json</InlineCode>:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="package.json" showChrome code={packageJsonCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="7" title="Run your app" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Finally, run your project with <InlineCode>npm run start</InlineCode>:
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="8" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="9" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
+4
@@ -28,6 +28,7 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
|
||||
|
||||
type SelectionChoices = "use-existing-project" | "create-new-next-app";
|
||||
|
||||
@@ -48,6 +49,9 @@ export default function SetupNextjs() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<NextjsLogo className="w-56" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
|
||||
|
||||
+4
@@ -27,6 +27,7 @@ import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { RemixLogo } from "~/assets/logos/RemixLogo";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Remix" />,
|
||||
@@ -43,6 +44,9 @@ export default function SetUpRemix() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<RemixLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { PrismaErrorSchema } from "~/db.server";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { CancelRunService } from "~/services/runs/cancelRun.server";
|
||||
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
// Authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const parsed = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsed.success) {
|
||||
return json({ error: "Invalid or Missing runId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { runId } = parsed.data;
|
||||
|
||||
const service = new CancelRunService();
|
||||
try {
|
||||
await service.call({ runId });
|
||||
} catch (error) {
|
||||
const prismaError = PrismaErrorSchema.safeParse(error);
|
||||
// Record not found in the database
|
||||
if (prismaError.success && prismaError.data.code === "P2005") {
|
||||
return json({ error: "Run not found" }, { status: 404 });
|
||||
} else {
|
||||
return json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
const presenter = new ApiRunPresenter();
|
||||
const jobRun = await presenter.call({
|
||||
runId: runId,
|
||||
});
|
||||
|
||||
if (!jobRun) {
|
||||
return json({ message: "Run not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({
|
||||
id: jobRun.id,
|
||||
status: jobRun.status,
|
||||
startedAt: jobRun.startedAt,
|
||||
updatedAt: jobRun.updatedAt,
|
||||
completedAt: jobRun.completedAt,
|
||||
output: jobRun.output,
|
||||
tasks: jobRun.tasks,
|
||||
statuses: jobRun.statuses.map((s) => ({
|
||||
...s,
|
||||
state: s.state ?? undefined,
|
||||
data: s.data ?? undefined,
|
||||
history: s.history ?? undefined,
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
id: z.string(),
|
||||
secret: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const { runId, id } = ParamsSchema.parse(params);
|
||||
|
||||
// Parse body as JSON (no schema parsing)
|
||||
const body = await request.json();
|
||||
|
||||
const service = new CallbackRunTaskService();
|
||||
|
||||
try {
|
||||
// Complete task with request body as output
|
||||
await service.call(runId, id, body, request.url);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Error while processing task callback:", { error });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export class CallbackRunTaskService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(runId: string, id: string, taskBody: any, callbackUrl: string): Promise<void> {
|
||||
const task = await findTask(prisma, id);
|
||||
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.runId !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.status !== "WAITING") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!task.callbackUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new URL(task.callbackUrl).pathname !== new URL(callbackUrl).pathname) {
|
||||
logger.error("Callback URLs don't match", { runId, taskId: id, callbackUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("CallbackRunTaskService.call()", { task });
|
||||
|
||||
await this.#resumeTask(task, taskBody);
|
||||
}
|
||||
|
||||
async #resumeTask(task: NonNullable<FoundTask>, output: any) {
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
await tx.taskAttempt.updateMany({
|
||||
where: {
|
||||
taskId: task.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
completedAt: new Date(),
|
||||
output: output ? output : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await this.#resumeRunExecution(task, tx);
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunExecution(task: NonNullable<FoundTask>, prisma: PrismaClientOrTransaction) {
|
||||
await enqueueRunExecutionV2(task.run, prisma, {
|
||||
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type FoundTask = Awaited<ReturnType<typeof findTask>>;
|
||||
|
||||
async function findTask(prisma: PrismaClientOrTransaction, id: string) {
|
||||
return prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
run: {
|
||||
include: {
|
||||
environment: true,
|
||||
queue: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
|
||||
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
|
||||
import { CompleteTaskBodyInputSchema } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -86,8 +86,8 @@ export class CompleteRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// Using a transaction, we'll first check to see if the task already exists and return if if it does
|
||||
// If it doesn't exist, we'll create it and return it
|
||||
const task = await this.#prismaClient.$transaction(async (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await this.#prismaClient.$transaction(async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,31 @@ export class CompleteRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -86,8 +86,8 @@ export class FailRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// Using a transaction, we'll first check to see if the task already exists and return if if it does
|
||||
// If it doesn't exist, we'll create it and return it
|
||||
const task = await this.#prismaClient.$transaction(async (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await this.#prismaClient.$transaction(async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,31 @@ export class FailRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { TaskStatus } from "@trigger.dev/database";
|
||||
import { RunTaskBodyOutput, RunTaskBodyOutputSchema, ServerTask } from "@trigger.dev/core";
|
||||
import {
|
||||
API_VERSIONS,
|
||||
RunTaskBodyOutput,
|
||||
RunTaskBodyOutputSchema,
|
||||
RunTaskResponseWithCachedTasksBody,
|
||||
ServerTask,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import { prepareTasksForCaching, taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { ulid } from "~/services/ulid.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { generateSecret } from "~/services/sources/utils.server";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
@@ -16,6 +24,8 @@ const ParamsSchema = z.object({
|
||||
|
||||
const HeadersSchema = z.object({
|
||||
"idempotency-key": z.string(),
|
||||
"trigger-version": z.string().optional().nullable(),
|
||||
"x-cached-tasks-cursor": z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
@@ -37,7 +47,11 @@ export async function action({ request, params }: ActionArgs) {
|
||||
return json({ error: "Invalid or Missing idempotency key" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { "idempotency-key": idempotencyKey } = headers.data;
|
||||
const {
|
||||
"idempotency-key": idempotencyKey,
|
||||
"trigger-version": triggerVersion,
|
||||
"x-cached-tasks-cursor": cachedTasksCursor,
|
||||
} = headers.data;
|
||||
|
||||
const { runId } = ParamsSchema.parse(params);
|
||||
|
||||
@@ -48,6 +62,8 @@ export async function action({ request, params }: ActionArgs) {
|
||||
body: anyBody,
|
||||
runId,
|
||||
idempotencyKey,
|
||||
triggerVersion,
|
||||
cachedTasksCursor,
|
||||
});
|
||||
|
||||
const body = RunTaskBodyOutputSchema.safeParse(anyBody);
|
||||
@@ -71,6 +87,26 @@ export async function action({ request, params }: ActionArgs) {
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
|
||||
if (triggerVersion === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) {
|
||||
const requestMigration = new ChangeRequestLazyLoadedCachedTasks();
|
||||
|
||||
const responseBody = await requestMigration.call(runId, task, cachedTasksCursor);
|
||||
|
||||
logger.debug(
|
||||
"RunTaskService.call() response migrating with ChangeRequestLazyLoadedCachedTasks",
|
||||
{
|
||||
responseBody,
|
||||
cachedTasksCursor,
|
||||
}
|
||||
);
|
||||
|
||||
return json(responseBody, {
|
||||
headers: {
|
||||
"trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return json(task);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -81,6 +117,51 @@ export async function action({ request, params }: ActionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChangeRequestLazyLoadedCachedTasks {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(
|
||||
runId: string,
|
||||
task: ServerTask,
|
||||
cursor?: string | null
|
||||
): Promise<RunTaskResponseWithCachedTasksBody> {
|
||||
if (!cursor) {
|
||||
return {
|
||||
task,
|
||||
};
|
||||
}
|
||||
|
||||
// We need to limit the cached tasks to not be too large >2MB when serialized
|
||||
const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000;
|
||||
|
||||
const nextTasks = await this.#prismaClient.task.findMany({
|
||||
where: {
|
||||
runId,
|
||||
status: "COMPLETED",
|
||||
noop: false,
|
||||
},
|
||||
take: 250,
|
||||
cursor: {
|
||||
id: cursor,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
|
||||
|
||||
return {
|
||||
task,
|
||||
cachedTasks: preparedTasks,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class RunTaskService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -106,10 +187,13 @@ export class RunTaskService {
|
||||
},
|
||||
});
|
||||
|
||||
const delayUntilInFuture = taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now();
|
||||
const callbackEnabled = taskBody.callback?.enabled;
|
||||
|
||||
if (existingTask) {
|
||||
if (existingTask.status === "CANCELED") {
|
||||
const existingTaskStatus =
|
||||
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
|
||||
delayUntilInFuture || callbackEnabled || taskBody.trigger
|
||||
? "WAITING"
|
||||
: taskBody.noop
|
||||
? "COMPLETED"
|
||||
@@ -154,16 +238,21 @@ export class RunTaskService {
|
||||
status = "CANCELED";
|
||||
} else {
|
||||
status =
|
||||
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
|
||||
delayUntilInFuture || callbackEnabled || taskBody.trigger
|
||||
? "WAITING"
|
||||
: taskBody.noop
|
||||
? "COMPLETED"
|
||||
: "RUNNING";
|
||||
}
|
||||
|
||||
const taskId = ulid();
|
||||
const callbackUrl = callbackEnabled
|
||||
? `${env.APP_ORIGIN}/api/v1/runs/${runId}/tasks/${taskId}/callback/${generateSecret(12)}`
|
||||
: undefined;
|
||||
|
||||
const task = await tx.task.create({
|
||||
data: {
|
||||
id: ulid(),
|
||||
id: taskId,
|
||||
idempotencyKey,
|
||||
displayKey: taskBody.displayKey,
|
||||
runConnection: taskBody.connectionKey
|
||||
@@ -194,6 +283,7 @@ export class RunTaskService {
|
||||
properties: taskBody.properties ?? undefined,
|
||||
redact: taskBody.redact ?? undefined,
|
||||
operation: taskBody.operation,
|
||||
callbackUrl,
|
||||
style: taskBody.style ?? { style: "normal" },
|
||||
attempts: {
|
||||
create: {
|
||||
@@ -217,6 +307,17 @@ export class RunTaskService {
|
||||
},
|
||||
{ tx, runAt: task.delayUntil ?? undefined }
|
||||
);
|
||||
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
|
||||
if (taskBody.callback.timeoutInSeconds > 0) {
|
||||
// We need to schedule the callback timeout
|
||||
await workerQueue.enqueue(
|
||||
"processCallbackTimeout",
|
||||
{
|
||||
id: task.id,
|
||||
},
|
||||
{ tx, runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
import { taskListToTree } from "~/utils/taskListToTree";
|
||||
@@ -51,51 +51,15 @@ export async function loader({ request, params }: LoaderArgs) {
|
||||
|
||||
const query = parsedQuery.data;
|
||||
const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE";
|
||||
|
||||
const take = Math.min(query.take, 50);
|
||||
|
||||
const jobRun = await prisma.jobRun.findUnique({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
startedAt: true,
|
||||
updatedAt: true,
|
||||
completedAt: true,
|
||||
environmentId: true,
|
||||
output: true,
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
displayKey: true,
|
||||
status: true,
|
||||
name: true,
|
||||
icon: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
params: showTaskDetails,
|
||||
output: showTaskDetails,
|
||||
},
|
||||
where: {
|
||||
parentId: query.subtasks ? undefined : null,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
take: take + 1,
|
||||
cursor: query.cursor
|
||||
? {
|
||||
id: query.cursor,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
statuses: {
|
||||
select: { key: true, label: true, state: true, data: true, history: true },
|
||||
},
|
||||
},
|
||||
const presenter = new ApiRunPresenter();
|
||||
const jobRun = await presenter.call({
|
||||
runId: runId,
|
||||
maxTasks: take,
|
||||
taskDetails: showTaskDetails,
|
||||
subTasks: query.subtasks,
|
||||
cursor: query.cursor,
|
||||
});
|
||||
|
||||
if (!jobRun) {
|
||||
|
||||
@@ -2,28 +2,15 @@ import { parse } from "@conform-to/zod";
|
||||
import { ActionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import {
|
||||
CreateEndpointError,
|
||||
CreateEndpointService,
|
||||
} from "~/services/endpoints/createEndpoint.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { RuntimeEnvironmentTypeSchema } from "@trigger.dev/core";
|
||||
import { env } from "process";
|
||||
import { CreateEndpointError } from "~/services/endpoints/createEndpoint.server";
|
||||
import { ValidateCreateEndpointService } from "~/services/endpoints/validateCreateEndpoint.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectId: z.string(),
|
||||
});
|
||||
|
||||
export const bodySchema = z.object({
|
||||
environmentId: z.string(),
|
||||
url: z.string().url("Must be a valid URL"),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectId } = ParamsSchema.parse(params);
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema: bodySchema });
|
||||
|
||||
@@ -48,7 +35,7 @@ export async function action({ request, params }: ActionArgs) {
|
||||
}
|
||||
|
||||
const service = new ValidateCreateEndpointService();
|
||||
const result = await service.call({
|
||||
await service.call({
|
||||
url: submission.value.url,
|
||||
environment,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ApiEventLog,
|
||||
DeliverEventResponseSchema,
|
||||
DeserializedJson,
|
||||
EndpointHeadersSchema,
|
||||
ErrorWithStackSchema,
|
||||
HttpSourceRequest,
|
||||
HttpSourceResponseSchema,
|
||||
@@ -89,6 +91,15 @@ export class EndpointApi {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (headers.success && headers.data["trigger-version"]) {
|
||||
return {
|
||||
...pongResponse.data,
|
||||
triggerVersion: headers.data["trigger-version"],
|
||||
};
|
||||
}
|
||||
|
||||
return pongResponse.data;
|
||||
}
|
||||
|
||||
@@ -129,41 +140,15 @@ export class EndpointApi {
|
||||
const anyBody = await response.json();
|
||||
|
||||
const data = IndexEndpointResponseSchema.parse(anyBody);
|
||||
const headers = EndpointHeadersSchema.parse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data,
|
||||
headers,
|
||||
} as const;
|
||||
}
|
||||
|
||||
async deliverEvent(event: ApiEventLog) {
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-trigger-api-key": this.apiKey,
|
||||
"x-trigger-action": "DELIVER_EVENT",
|
||||
},
|
||||
body: JSON.stringify(event),
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`);
|
||||
}
|
||||
|
||||
const anyBody = await response.json();
|
||||
|
||||
logger.debug("deliverEvent() response from endpoint", {
|
||||
body: anyBody,
|
||||
});
|
||||
|
||||
return DeliverEventResponseSchema.parse(anyBody);
|
||||
}
|
||||
|
||||
async executeJobRequest(options: RunJobBody) {
|
||||
const startTimeInMs = performance.now();
|
||||
|
||||
@@ -338,6 +323,15 @@ export class EndpointApi {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (headers.success && headers.data["trigger-version"]) {
|
||||
return {
|
||||
...validateResponse.data,
|
||||
triggerVersion: headers.data["trigger-version"],
|
||||
};
|
||||
}
|
||||
|
||||
return validateResponse.data;
|
||||
}
|
||||
}
|
||||
@@ -359,6 +353,7 @@ function addStandardRequestOptions(options: RequestInit) {
|
||||
headers: {
|
||||
...options.headers,
|
||||
"user-agent": "triggerdotdev-server/2.0.0",
|
||||
"x-trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,9 +74,11 @@ export class CreateEndpointService {
|
||||
slug: id,
|
||||
url: endpointUrl,
|
||||
indexingHookIdentifier: indexingHookIdentifier(),
|
||||
version: pong.triggerVersion,
|
||||
},
|
||||
update: {
|
||||
url: endpointUrl,
|
||||
version: pong.triggerVersion,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export class IndexEndpointService {
|
||||
}
|
||||
|
||||
const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data;
|
||||
const { "trigger-version": triggerVersion } = indexResponse.headers;
|
||||
|
||||
logger.debug("Indexing endpoint", {
|
||||
endpointId: endpoint.id,
|
||||
@@ -48,6 +49,7 @@ export class IndexEndpointService {
|
||||
endpointSlug: endpoint.slug,
|
||||
source: source,
|
||||
sourceData: sourceData,
|
||||
triggerVersion,
|
||||
stats: {
|
||||
jobs: jobs.length,
|
||||
sources: sources.length,
|
||||
@@ -56,6 +58,17 @@ export class IndexEndpointService {
|
||||
},
|
||||
});
|
||||
|
||||
if (triggerVersion && triggerVersion !== endpoint.version) {
|
||||
await this.#prismaClient.endpoint.update({
|
||||
where: {
|
||||
id: endpoint.id,
|
||||
},
|
||||
data: {
|
||||
version: triggerVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const indexStats = {
|
||||
jobs: 0,
|
||||
sources: 0,
|
||||
|
||||
@@ -58,9 +58,11 @@ export class ValidateCreateEndpointService {
|
||||
slug: validationResult.endpointId,
|
||||
url: endpointUrl,
|
||||
indexingHookIdentifier: indexingHookIdentifier(),
|
||||
version: validationResult.triggerVersion,
|
||||
},
|
||||
update: {
|
||||
url: endpointUrl,
|
||||
version: validationResult.triggerVersion,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -34,77 +34,55 @@ export class IngestSendEvent {
|
||||
try {
|
||||
const deliverAt = this.#calculateDeliverAt(options);
|
||||
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
const externalAccount = options?.accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
const externalAccount = options?.accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// Create a new event in the database
|
||||
const eventLog = await tx.eventRecord.create({
|
||||
data: {
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
project: {
|
||||
connect: {
|
||||
id: environment.projectId,
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: environment.id,
|
||||
},
|
||||
},
|
||||
eventId: event.id,
|
||||
name: event.name,
|
||||
timestamp: event.timestamp ?? new Date(),
|
||||
payload: event.payload ?? {},
|
||||
context: event.context ?? {},
|
||||
source: event.source ?? "trigger.dev",
|
||||
sourceContext,
|
||||
deliverAt: deliverAt,
|
||||
externalAccount: externalAccount
|
||||
? {
|
||||
connect: {
|
||||
id: externalAccount.id,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
// Create a new event in the database
|
||||
const eventLog = await tx.eventRecord.create({
|
||||
data: {
|
||||
organizationId: environment.organizationId,
|
||||
projectId: environment.projectId,
|
||||
environmentId: environment.id,
|
||||
eventId: event.id,
|
||||
name: event.name,
|
||||
timestamp: event.timestamp ?? new Date(),
|
||||
payload: event.payload ?? {},
|
||||
context: event.context ?? {},
|
||||
source: event.source ?? "trigger.dev",
|
||||
sourceContext,
|
||||
deliverAt: deliverAt,
|
||||
externalAccountId: externalAccount ? externalAccount.id : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (this.deliverEvents) {
|
||||
// Produce a message to the event bus
|
||||
await workerQueue.enqueue(
|
||||
"deliverEvent",
|
||||
{
|
||||
id: eventLog.id,
|
||||
},
|
||||
});
|
||||
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
|
||||
);
|
||||
}
|
||||
|
||||
if (this.deliverEvents) {
|
||||
// Produce a message to the event bus
|
||||
await workerQueue.enqueue(
|
||||
"deliverEvent",
|
||||
{
|
||||
id: eventLog.id,
|
||||
},
|
||||
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
|
||||
);
|
||||
}
|
||||
|
||||
return eventLog;
|
||||
},
|
||||
{ rethrowPrismaErrors: true }
|
||||
);
|
||||
return eventLog;
|
||||
});
|
||||
} catch (error) {
|
||||
const prismaError = PrismaErrorSchema.safeParse(error);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { github } from "./integrations/github";
|
||||
import { linear } from "./integrations/linear";
|
||||
import { openai } from "./integrations/openai";
|
||||
import { plain } from "./integrations/plain";
|
||||
import { replicate } from "./integrations/replicate";
|
||||
import { resend } from "./integrations/resend";
|
||||
import { sendgrid } from "./integrations/sendgrid";
|
||||
import { slack } from "./integrations/slack";
|
||||
@@ -37,6 +38,7 @@ export const integrationCatalog = new IntegrationCatalog({
|
||||
linear,
|
||||
openai,
|
||||
plain,
|
||||
replicate,
|
||||
resend,
|
||||
slack,
|
||||
stripe,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { HelpSample, Integration } from "../types";
|
||||
|
||||
function usageSample(hasApiKey: boolean): HelpSample {
|
||||
const apiKeyPropertyName = "apiKey";
|
||||
|
||||
return {
|
||||
title: "Using the client",
|
||||
code: `
|
||||
import { Replicate } from "@trigger.dev/replicate";
|
||||
|
||||
const replicate = new Replicate({
|
||||
id: "__SLUG__",${hasApiKey ? `,\n ${apiKeyPropertyName}: process.env.REPLICATE_API_KEY!` : ""}
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "replicate-create-prediction",
|
||||
name: "Replicate - Create Prediction",
|
||||
version: "0.1.0",
|
||||
integrations: { replicate },
|
||||
trigger: eventTrigger({
|
||||
name: "replicate.predict",
|
||||
schema: z.object({
|
||||
prompt: z.string(),
|
||||
version: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
return io.replicate.predictions.createAndAwait("await-prediction", {
|
||||
version: payload.version,
|
||||
input: { prompt: payload.prompt },
|
||||
});
|
||||
},
|
||||
});
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
export const replicate: Integration = {
|
||||
identifier: "replicate",
|
||||
name: "Replicate",
|
||||
packageName: "@trigger.dev/replicate@latest",
|
||||
authenticationMethods: {
|
||||
apikey: {
|
||||
type: "apikey",
|
||||
help: {
|
||||
samples: [usageSample(true)],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -42,29 +42,32 @@ export class CreateRunService {
|
||||
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
// Get the current max number for the given jobId
|
||||
const currentMaxNumber = await tx.jobRun.aggregate({
|
||||
const latestJob = await tx.jobRun.findFirst({
|
||||
where: { jobId: job.id },
|
||||
_max: { number: true },
|
||||
orderBy: { id: "desc" },
|
||||
select: {
|
||||
number: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Increment the number for the new execution
|
||||
const newNumber = (currentMaxNumber._max.number ?? 0) + 1;
|
||||
const newNumber = (latestJob?.number ?? 0) + 1;
|
||||
|
||||
// Create the new execution with the incremented number
|
||||
const run = await tx.jobRun.create({
|
||||
data: {
|
||||
number: newNumber,
|
||||
preprocess: version.preprocessRuns,
|
||||
job: { connect: { id: job.id } },
|
||||
version: { connect: { id: version.id } },
|
||||
event: { connect: { id: eventId } },
|
||||
environment: { connect: { id: environment.id } },
|
||||
organization: { connect: { id: environment.organizationId } },
|
||||
project: { connect: { id: environment.projectId } },
|
||||
endpoint: { connect: { id: endpoint.id } },
|
||||
queue: { connect: { id: jobQueue.id } },
|
||||
externalAccount: eventRecord.externalAccountId
|
||||
? { connect: { id: eventRecord.externalAccountId } }
|
||||
jobId: job.id,
|
||||
versionId: version.id,
|
||||
eventId: eventId,
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
projectId: environment.projectId,
|
||||
endpointId: endpoint.id,
|
||||
queueId: jobQueue.id,
|
||||
externalAccountId: eventRecord.externalAccountId
|
||||
? eventRecord.externalAccountId
|
||||
: undefined,
|
||||
isTest: eventRecord.isTest,
|
||||
},
|
||||
|
||||
@@ -263,6 +263,7 @@ export class PerformRunExecutionV1Service {
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.map((t) => CachedTaskSchema.parse(t)),
|
||||
yieldedExecutions: run.yieldedExecutions,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
@@ -354,6 +355,11 @@ export class PerformRunExecutionV1Service {
|
||||
|
||||
break;
|
||||
}
|
||||
case "YIELD_EXECUTION": {
|
||||
await this.#resumeYieldedExecution(execution, safeBody.data.key);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
@@ -393,6 +399,40 @@ export class PerformRunExecutionV1Service {
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeYieldedExecution(execution: FoundRunExecution, key: string) {
|
||||
const { run } = execution;
|
||||
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await tx.jobRunExecution.update({
|
||||
where: {
|
||||
id: execution.id,
|
||||
},
|
||||
data: {
|
||||
status: "SUCCESS",
|
||||
completedAt: new Date(),
|
||||
run: {
|
||||
update: {
|
||||
yieldedExecutions: {
|
||||
push: key,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newJobExecution = await tx.jobRunExecution.create({
|
||||
data: {
|
||||
runId: run.id,
|
||||
reason: "EXECUTE_JOB",
|
||||
status: "PENDING",
|
||||
retryLimit: EXECUTE_JOB_RETRY_LIMIT,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueRunExecutionV1(newJobExecution, run.queue.id, run.queue.maxJobs, tx);
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunWithTask(execution: FoundRunExecution, data: RunJobResumeWithTask) {
|
||||
const { run } = execution;
|
||||
|
||||
@@ -409,7 +449,9 @@ export class PerformRunExecutionV1Service {
|
||||
|
||||
// If the task has an operation, then the next performRunExecution will occur
|
||||
// when that operation has finished
|
||||
if (!data.task.operation) {
|
||||
// Tasks with callbacks enabled will also get processed separately, i.e. when
|
||||
// they time out, or on valid requests to their callbackUrl
|
||||
if (!data.task.operation && !data.task.callbackUrl) {
|
||||
const newJobExecution = await tx.jobRunExecution.create({
|
||||
data: {
|
||||
runId: run.id,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {
|
||||
CachedTask,
|
||||
API_VERSIONS,
|
||||
BloomFilter,
|
||||
ConnectionAuth,
|
||||
EndpointHeadersSchema,
|
||||
RunJobError,
|
||||
RunJobInvalidPayloadError,
|
||||
RunJobResumeWithTask,
|
||||
RunJobRetryWithTask,
|
||||
RunJobSuccess,
|
||||
RunJobUnresolvedAuthError,
|
||||
RunSourceContext,
|
||||
RunSourceContextSchema,
|
||||
supportsFeature,
|
||||
} from "@trigger.dev/core";
|
||||
import { RuntimeEnvironmentType, type Task } from "@trigger.dev/database";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
@@ -18,10 +23,17 @@ import { formatError } from "~/utils/formatErrors.server";
|
||||
import { safeJsonZodParse } from "~/utils/json";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { prepareTasksForCaching, prepareTasksForCachingLegacy } from "~/models/task.server";
|
||||
import { MAX_RUN_YIELDED_EXECUTIONS } from "~/consts";
|
||||
import { ApiEventLog } from "@trigger.dev/core";
|
||||
import { RunJobBody } from "@trigger.dev/core";
|
||||
|
||||
type FoundRun = NonNullable<Awaited<ReturnType<typeof findRun>>>;
|
||||
type FoundTask = FoundRun["tasks"][number];
|
||||
|
||||
// We need to limit the cached tasks to not be too large >3.5MB when serialized
|
||||
const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000;
|
||||
|
||||
export type PerformRunExecutionV2Input = {
|
||||
id: string;
|
||||
reason: "PREPROCESS" | "EXECUTE_JOB";
|
||||
@@ -153,6 +165,29 @@ export class PerformRunExecutionV2Service {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
typeof process.env.BLOCKED_ORGS === "string" &&
|
||||
process.env.BLOCKED_ORGS.includes(run.organizationId)
|
||||
) {
|
||||
logger.debug("Skipping execution for blocked org", {
|
||||
orgId: run.organizationId,
|
||||
});
|
||||
|
||||
await this.#prismaClient.jobRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
status: "CANCELED",
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
|
||||
const event = eventRecordToApiJson(run.event);
|
||||
|
||||
@@ -207,38 +242,19 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
const sourceContext = RunSourceContextSchema.safeParse(run.event.sourceContext);
|
||||
|
||||
const { response, parser, errorParser, durationInMs } = await client.executeJobRequest({
|
||||
const executionBody = await this.#createExecutionBody(
|
||||
run,
|
||||
[run.tasks, resumedTask].flat().filter(Boolean),
|
||||
startedAt,
|
||||
isRetry,
|
||||
connections.auth,
|
||||
event,
|
||||
job: {
|
||||
id: run.version.job.slug,
|
||||
version: run.version.version,
|
||||
},
|
||||
run: {
|
||||
id: run.id,
|
||||
isTest: run.isTest,
|
||||
startedAt,
|
||||
isRetry,
|
||||
},
|
||||
environment: {
|
||||
id: run.environment.id,
|
||||
slug: run.environment.slug,
|
||||
type: run.environment.type,
|
||||
},
|
||||
organization: {
|
||||
id: run.organization.id,
|
||||
slug: run.organization.slug,
|
||||
title: run.organization.title,
|
||||
},
|
||||
account: run.externalAccount
|
||||
? {
|
||||
id: run.externalAccount.identifier,
|
||||
metadata: run.externalAccount.metadata,
|
||||
}
|
||||
: undefined,
|
||||
connections: connections.auth,
|
||||
source: sourceContext.success ? sourceContext.data : undefined,
|
||||
tasks: prepareTasksForRun([run.tasks, resumedTask].flat().filter(Boolean)),
|
||||
});
|
||||
sourceContext.success ? sourceContext.data : undefined
|
||||
);
|
||||
|
||||
const { response, parser, errorParser, durationInMs } = await client.executeJobRequest(
|
||||
executionBody
|
||||
);
|
||||
|
||||
if (!response) {
|
||||
return await this.#failRunExecutionWithRetry({
|
||||
@@ -246,6 +262,25 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
|
||||
// Update the endpoint version if it has changed
|
||||
const rawHeaders = Object.fromEntries(response.headers.entries());
|
||||
const headers = EndpointHeadersSchema.safeParse(rawHeaders);
|
||||
|
||||
if (
|
||||
headers.success &&
|
||||
headers.data["trigger-version"] &&
|
||||
headers.data["trigger-version"] !== run.endpoint.version
|
||||
) {
|
||||
await this.#prismaClient.endpoint.update({
|
||||
where: {
|
||||
id: run.endpoint.id,
|
||||
},
|
||||
data: {
|
||||
version: headers.data["trigger-version"],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const rawBody = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -366,6 +401,10 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
break;
|
||||
}
|
||||
case "YIELD_EXECUTION": {
|
||||
await this.#resumeYieldedRun(run, safeBody.data.key, isRetry, durationInMs, executionCount);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
@@ -373,6 +412,91 @@ export class PerformRunExecutionV2Service {
|
||||
}
|
||||
}
|
||||
|
||||
async #createExecutionBody(
|
||||
run: FoundRun,
|
||||
tasks: FoundTask[],
|
||||
startedAt: Date,
|
||||
isRetry: boolean,
|
||||
connections: Record<string, ConnectionAuth>,
|
||||
event: ApiEventLog,
|
||||
source?: RunSourceContext
|
||||
): Promise<RunJobBody> {
|
||||
if (supportsFeature("lazyLoadedCachedTasks", run.endpoint.version)) {
|
||||
const preparedTasks = prepareTasksForCaching(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
|
||||
|
||||
return {
|
||||
event,
|
||||
job: {
|
||||
id: run.version.job.slug,
|
||||
version: run.version.version,
|
||||
},
|
||||
run: {
|
||||
id: run.id,
|
||||
isTest: run.isTest,
|
||||
startedAt,
|
||||
isRetry,
|
||||
},
|
||||
environment: {
|
||||
id: run.environment.id,
|
||||
slug: run.environment.slug,
|
||||
type: run.environment.type,
|
||||
},
|
||||
organization: {
|
||||
id: run.organization.id,
|
||||
slug: run.organization.slug,
|
||||
title: run.organization.title,
|
||||
},
|
||||
account: run.externalAccount
|
||||
? {
|
||||
id: run.externalAccount.identifier,
|
||||
metadata: run.externalAccount.metadata,
|
||||
}
|
||||
: undefined,
|
||||
connections,
|
||||
source,
|
||||
tasks: preparedTasks.tasks,
|
||||
cachedTaskCursor: preparedTasks.cursor,
|
||||
noopTasksSet: prepareNoOpTasksBloomFilter(tasks),
|
||||
yieldedExecutions: run.yieldedExecutions,
|
||||
};
|
||||
}
|
||||
|
||||
const preparedTasks = prepareTasksForCachingLegacy(tasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
|
||||
|
||||
return {
|
||||
event,
|
||||
job: {
|
||||
id: run.version.job.slug,
|
||||
version: run.version.version,
|
||||
},
|
||||
run: {
|
||||
id: run.id,
|
||||
isTest: run.isTest,
|
||||
startedAt,
|
||||
isRetry,
|
||||
},
|
||||
environment: {
|
||||
id: run.environment.id,
|
||||
slug: run.environment.slug,
|
||||
type: run.environment.type,
|
||||
},
|
||||
organization: {
|
||||
id: run.organization.id,
|
||||
slug: run.organization.slug,
|
||||
title: run.organization.title,
|
||||
},
|
||||
account: run.externalAccount
|
||||
? {
|
||||
id: run.externalAccount.identifier,
|
||||
metadata: run.externalAccount.metadata,
|
||||
}
|
||||
: undefined,
|
||||
connections,
|
||||
source,
|
||||
tasks: preparedTasks.tasks,
|
||||
};
|
||||
}
|
||||
|
||||
async #completeRunWithSuccess(run: FoundRun, data: RunJobSuccess, durationInMs: number) {
|
||||
await this.#prismaClient.jobRun.update({
|
||||
where: { id: run.id },
|
||||
@@ -406,7 +530,9 @@ export class PerformRunExecutionV2Service {
|
||||
|
||||
// If the task has an operation, then the next performRunExecution will occur
|
||||
// when that operation has finished
|
||||
if (!data.task.operation) {
|
||||
// Tasks with callbacks enabled will also get processed separately, i.e. when
|
||||
// they time out, or on valid requests to their callbackUrl
|
||||
if (!data.task.operation && !data.task.callbackUrl) {
|
||||
await enqueueRunExecutionV2(run, tx, {
|
||||
runAt: data.task.delayUntil ?? undefined,
|
||||
resumeTaskId: data.task.id,
|
||||
@@ -478,6 +604,56 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeYieldedRun(
|
||||
run: FoundRun,
|
||||
key: string,
|
||||
isRetry: boolean,
|
||||
durationInMs: number,
|
||||
executionCount: number
|
||||
) {
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
if (run.yieldedExecutions.length + 1 > MAX_RUN_YIELDED_EXECUTIONS) {
|
||||
return await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
run,
|
||||
{
|
||||
message: `Run has yielded too many times, the maximum is ${MAX_RUN_YIELDED_EXECUTIONS}`,
|
||||
},
|
||||
"FAILURE",
|
||||
durationInMs
|
||||
);
|
||||
}
|
||||
|
||||
await tx.jobRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
executionDuration: {
|
||||
increment: durationInMs,
|
||||
},
|
||||
executionCount: {
|
||||
increment: 1,
|
||||
},
|
||||
yieldedExecutions: {
|
||||
push: key,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
yieldedExecutions: true,
|
||||
executionCount: true,
|
||||
},
|
||||
});
|
||||
|
||||
await enqueueRunExecutionV2(run, tx, {
|
||||
isRetry,
|
||||
skipRetrying: run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
executionCount,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async #retryRunWithTask(
|
||||
run: FoundRun,
|
||||
data: RunJobRetryWithTask,
|
||||
@@ -663,69 +839,16 @@ export class PerformRunExecutionV2Service {
|
||||
}
|
||||
}
|
||||
|
||||
function prepareTasksForRun(possibleTasks: FoundTask[]): CachedTask[] {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED");
|
||||
function prepareNoOpTasksBloomFilter(possibleTasks: FoundTask[]): string {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && task.noop);
|
||||
|
||||
// We need to limit the cached tasks to not be too large >3.5MB when serialized
|
||||
const TOTAL_CACHED_TASK_BYTE_LIMIT = 3500000;
|
||||
const filter = new BloomFilter(BloomFilter.NOOP_TASK_SET_SIZE);
|
||||
|
||||
const cachedTasks = new Map<string, CachedTask>(); // Cache for prepared tasks
|
||||
const cachedTaskSizes = new Map<string, number>(); // Cache for calculated task sizes
|
||||
|
||||
// Helper function to get the cached prepared task, or prepare and cache if not already cached
|
||||
function getCachedTask(task: FoundTask): CachedTask {
|
||||
const taskId = task.id;
|
||||
if (!cachedTasks.has(taskId)) {
|
||||
cachedTasks.set(taskId, prepareTaskForRun(task));
|
||||
}
|
||||
return cachedTasks.get(taskId)!;
|
||||
for (const task of tasks) {
|
||||
filter.add(task.idempotencyKey);
|
||||
}
|
||||
|
||||
// Helper function to get the cached task size, or calculate and cache if not already cached
|
||||
function getCachedTaskSize(task: CachedTask): number {
|
||||
const taskId = task.id;
|
||||
if (!cachedTaskSizes.has(taskId)) {
|
||||
cachedTaskSizes.set(taskId, calculateCachedTaskSize(task));
|
||||
}
|
||||
return cachedTaskSizes.get(taskId)!;
|
||||
}
|
||||
|
||||
// Prepare tasks and calculate their sizes
|
||||
const availableTasks = tasks.map((task) => {
|
||||
const cachedTask = getCachedTask(task);
|
||||
return { task: cachedTask, size: getCachedTaskSize(cachedTask) };
|
||||
});
|
||||
|
||||
// Sort tasks in ascending order by size
|
||||
availableTasks.sort((a, b) => a.size - b.size);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = TOTAL_CACHED_TASK_BYTE_LIMIT;
|
||||
|
||||
for (const { task, size } of availableTasks) {
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(task);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return tasksToRun;
|
||||
}
|
||||
|
||||
function prepareTaskForRun(task: FoundTask): CachedTask {
|
||||
return {
|
||||
id: task.idempotencyKey, // We should eventually move this back to task.id
|
||||
status: task.status,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
noop: task.noop,
|
||||
output: task.output as any,
|
||||
parentId: task.parentId,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateCachedTaskSize(task: CachedTask): number {
|
||||
return JSON.stringify(task).length;
|
||||
return filter.serialize();
|
||||
}
|
||||
|
||||
async function findRun(prisma: PrismaClientOrTransaction, id: string) {
|
||||
@@ -760,6 +883,9 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) {
|
||||
output: true,
|
||||
parentId: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
version: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export function generateSecret(): string {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
export function generateSecret(sizeInBytes = 32): string {
|
||||
return crypto.randomBytes(sizeInBytes).toString("hex");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { env } from "process";
|
||||
import { Run } from "~/presenters/RunPresenter.server";
|
||||
import {
|
||||
FetchOperationSchema,
|
||||
FetchRequestInit,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
|
||||
import { logger } from "../logger.server";
|
||||
|
||||
type FoundTask = Awaited<ReturnType<typeof findTask>>;
|
||||
|
||||
export class ProcessCallbackTimeoutService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(id: string) {
|
||||
const task = await findTask(this.#prismaClient, id);
|
||||
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.status !== "WAITING" || !task.callbackUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("ProcessCallbackTimeoutService.call", { task });
|
||||
|
||||
return await this.#failTask(task, "Remote callback timeout - no requests received");
|
||||
}
|
||||
|
||||
async #failTask(task: NonNullable<FoundTask>, error: string) {
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
await tx.taskAttempt.updateMany({
|
||||
where: {
|
||||
taskId: task.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
error
|
||||
},
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
completedAt: new Date(),
|
||||
output: error,
|
||||
},
|
||||
});
|
||||
|
||||
await this.#resumeRunExecution(task, tx);
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunExecution(task: NonNullable<FoundTask>, prisma: PrismaClientOrTransaction) {
|
||||
await enqueueRunExecutionV2(task.run, prisma, {
|
||||
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function findTask(prisma: PrismaClient, id: string) {
|
||||
return prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
run: {
|
||||
include: {
|
||||
environment: true,
|
||||
queue: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { DeliverScheduledEventService } from "./schedules/deliverScheduledEvent.
|
||||
import { ActivateSourceService } from "./sources/activateSource.server";
|
||||
import { DeliverHttpSourceRequestService } from "./sources/deliverHttpSourceRequest.server";
|
||||
import { PerformTaskOperationService } from "./tasks/performTaskOperation.server";
|
||||
import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout";
|
||||
import { addMissingVersionField } from "@trigger.dev/core";
|
||||
|
||||
const workerCatalog = {
|
||||
@@ -30,6 +31,9 @@ const workerCatalog = {
|
||||
}),
|
||||
scheduleEmail: DeliverEmailSchema,
|
||||
startRun: z.object({ id: z.string() }),
|
||||
processCallbackTimeout: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
performTaskOperation: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
@@ -161,7 +165,8 @@ function getWorkerQueue() {
|
||||
tasks: {
|
||||
"events.invokeDispatcher": {
|
||||
priority: 0, // smaller number = higher priority
|
||||
maxAttempts: 3,
|
||||
maxAttempts: 6,
|
||||
queueName: (payload) => `dispatcher:${payload.id}`, // use a queue for a dispatcher so runs are created sequentially
|
||||
handler: async (payload, job) => {
|
||||
const service = new InvokeDispatcherService();
|
||||
|
||||
@@ -239,6 +244,15 @@ function getWorkerQueue() {
|
||||
await service.call(payload.id);
|
||||
},
|
||||
},
|
||||
processCallbackTimeout: {
|
||||
priority: 0, // smaller number = higher priority
|
||||
maxAttempts: 3,
|
||||
handler: async (payload, job) => {
|
||||
const service = new ProcessCallbackTimeoutService();
|
||||
|
||||
await service.call(payload.id);
|
||||
},
|
||||
},
|
||||
performTaskOperation: {
|
||||
priority: 0, // smaller number = higher priority
|
||||
queueName: (payload) => `tasks:${payload.id}`,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Redacts the given object based on the given paths
|
||||
// Example:
|
||||
// const redactor = new Redactor(["data.object.balance_transaction"]);
|
||||
// redactor.redact({
|
||||
// data: {
|
||||
// object: {
|
||||
// balance_transaction: "txn_1NYWgTI0XSgju2urW3aXpinM",
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// Returns:
|
||||
// {
|
||||
// data: {
|
||||
// object: {
|
||||
// balance_transaction: "[REDACTED]",
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// Does not currenly support arrays
|
||||
export class Redactor {
|
||||
constructor(private paths: string[]) {}
|
||||
|
||||
public redact(subject: unknown): unknown {
|
||||
if (!Array.isArray(this.paths)) {
|
||||
return subject;
|
||||
}
|
||||
|
||||
if (this.paths.length === 0) {
|
||||
return subject;
|
||||
}
|
||||
|
||||
const clonedSubject = JSON.parse(JSON.stringify(subject));
|
||||
|
||||
return this.redactPathsRecursive(clonedSubject, this.paths);
|
||||
}
|
||||
|
||||
private redactPathsRecursive(subject: any, paths: string[]): any {
|
||||
for (let path of paths) {
|
||||
let parts = path.split(".");
|
||||
|
||||
let curSubject = subject;
|
||||
|
||||
// Make sure curSubject is an object
|
||||
if (typeof curSubject !== "object") {
|
||||
break;
|
||||
}
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(curSubject, part) === false) {
|
||||
// Path is not found in object
|
||||
break;
|
||||
}
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
// We're at the end of our path and have a string, redact it
|
||||
curSubject[part] = "[REDACTED]";
|
||||
} else if (part in curSubject && typeof curSubject[part] === "object") {
|
||||
// More paths to follow, continue down the path
|
||||
curSubject = curSubject[part];
|
||||
} else {
|
||||
// Path is not found in object or doesn't point to a string
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/language": "^6.3.1",
|
||||
"@codemirror/lint": "^6.4.2",
|
||||
"@codemirror/search": "^6.2.3",
|
||||
"@codemirror/state": "^6.1.3",
|
||||
"@codemirror/view": "^6.5.0",
|
||||
@@ -61,8 +62,8 @@
|
||||
"@remix-run/server-runtime": "1.19.2-pre.0",
|
||||
"@team-plain/typescript-sdk": "^2.2.0",
|
||||
"@trigger.dev/companyicons": "^1.5.14",
|
||||
"@trigger.dev/database": "workspace:*",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/database": "workspace:*",
|
||||
"@trigger.dev/sdk": "workspace:*",
|
||||
"@uiw/react-codemirror": "^4.19.5",
|
||||
"class-variance-authority": "^0.5.2",
|
||||
@@ -73,7 +74,6 @@
|
||||
"cuid": "^2.1.8",
|
||||
"emails": "workspace:*",
|
||||
"express": "^4.18.1",
|
||||
"fast-redact": "^3.1.2",
|
||||
"framer-motion": "^10.12.11",
|
||||
"graphile-worker": "^0.13.0",
|
||||
"highlight.run": "^7.3.4",
|
||||
@@ -94,8 +94,9 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-hotkeys-hook": "^3.4.7",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-use": "^17.4.0",
|
||||
"recharts": "^2.8.0",
|
||||
"remix-auth": "^3.2.2",
|
||||
"remix-auth-email-link": "^1.4.2",
|
||||
"remix-auth-github": "^1.1.1",
|
||||
@@ -110,7 +111,7 @@
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"ulid": "^2.3.0",
|
||||
"zod": "3.21.4",
|
||||
"zod": "3.22.3",
|
||||
"zod-error": "1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { integrationCatalog } from "../app/services/externalApis/integrationCatalog.server";
|
||||
import { seedCloud } from "./seedCloud";
|
||||
import { prisma } from "../app/db.server";
|
||||
import { createEnvironment } from "~/models/organization.server";
|
||||
|
||||
async function seedIntegrationAuthMethods() {
|
||||
for (const [_, integration] of Object.entries(integrationCatalog.getIntegrations())) {
|
||||
@@ -67,12 +68,78 @@ async function seedIntegrationAuthMethods() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runDataMigrations() {
|
||||
await runStagingEnvironmentMigration();
|
||||
}
|
||||
|
||||
async function runStagingEnvironmentMigration() {
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const existingDataMigration = await tx.dataMigration.findUnique({
|
||||
where: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
});
|
||||
|
||||
if (existingDataMigration) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tx.dataMigration.create({
|
||||
data: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Running data migration 2023-09-27-AddStagingEnvironments");
|
||||
|
||||
const projectsWithoutStagingEnvironments = await tx.project.findMany({
|
||||
where: {
|
||||
environments: {
|
||||
none: {
|
||||
type: "STAGING",
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const project of projectsWithoutStagingEnvironments) {
|
||||
try {
|
||||
console.log(
|
||||
`Creating staging environment for project ${project.slug} on org ${project.organization.slug}`
|
||||
);
|
||||
|
||||
await createEnvironment(project.organization, project, "STAGING", undefined, tx);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
await tx.dataMigration.update({
|
||||
where: {
|
||||
name: "2023-09-27-AddStagingEnvironments",
|
||||
},
|
||||
data: {
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
await seedIntegrationAuthMethods();
|
||||
|
||||
if (process.env.NODE_ENV === "development" && process.env.SEED_CLOUD === "enabled") {
|
||||
await seedCloud(prisma);
|
||||
}
|
||||
|
||||
await runDataMigrations();
|
||||
}
|
||||
|
||||
seed()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./node18.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"paths": {
|
||||
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
|
||||
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
|
||||
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
|
||||
"@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"]
|
||||
},
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"baseUrl": ".",
|
||||
"stripInternal": true
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
|
||||
## Install and initial setup
|
||||
|
||||
`npm install`
|
||||
`pnpm install`
|
||||
|
||||
## Running the app
|
||||
|
||||
`npm run dev`
|
||||
`pnpm run dev --filter docs`
|
||||
|
||||
## View the app locally
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<Card title="React hooks" icon="fishing-rod" href="/documentation/guides/react-hooks">
|
||||
Show the live status of Job Runs in your React app
|
||||
</Card>
|
||||
@@ -0,0 +1,64 @@
|
||||
<Card
|
||||
icon={
|
||||
<svg
|
||||
width="150"
|
||||
height="49"
|
||||
viewBox="-38 0 226 49"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_5336_22429)">
|
||||
<mask id="mask0_5336_22429" maskUnits="userSpaceOnUse" x="0" y="0" width="184" height="49">
|
||||
<path d="M184 0H0V48.4533H184V0Z" fill="white" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_5336_22429)">
|
||||
<path
|
||||
d="M12.4424 40.9986C10.2625 39.0108 9.62608 34.8341 10.5343 31.8083C12.1092 33.716 14.2912 34.3203 16.5514 34.6615C20.0406 35.1879 23.4674 34.991 26.7087 33.4002C27.0795 33.218 27.4221 32.9759 27.8273 32.7306C28.1315 33.6107 28.2106 34.4993 28.1044 35.4037C27.8461 37.6063 26.7472 39.3077 24.9995 40.5974C24.3006 41.1133 23.5611 41.5744 22.8393 42.0609C20.6218 43.5559 20.0219 45.3089 20.8551 47.8589C20.8749 47.921 20.8926 47.9831 20.9374 48.1347C19.8053 47.6293 18.9783 46.8934 18.3481 45.9258C17.6825 44.9046 17.3659 43.7749 17.3492 42.5525C17.3409 41.9577 17.3409 41.3576 17.2607 40.7711C17.0649 39.3413 16.3921 38.7013 15.1245 38.6644C13.8236 38.6265 12.7945 39.4287 12.5216 40.6921C12.5008 40.789 12.4706 40.8849 12.4404 40.9975L12.4424 40.9986Z"
|
||||
fill="url(#paint0_linear_5336_22429)"
|
||||
/>
|
||||
<path
|
||||
d="M0 31.3041C0 31.3041 6.45527 28.1673 12.9286 28.1673L17.8093 13.1001C17.992 12.3714 18.5256 11.8762 19.1278 11.8762C19.7302 11.8762 20.2637 12.3714 20.4464 13.1001L25.3271 28.1673C32.9938 28.1673 38.2557 31.3041 38.2557 31.3041C38.2557 31.3041 27.2909 1.50808 27.2694 1.44829C26.9547 0.567361 26.4234 0 25.7072 0H12.5496C11.8333 0 11.3235 0.567361 10.9874 1.44829C10.9637 1.50695 0 31.3041 0 31.3041Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M68.0598 26.9653C68.0598 29.6056 64.7674 31.1825 60.2089 31.1825C57.2422 31.1825 56.1929 30.4491 56.1929 28.9089C56.1929 27.2954 57.4954 26.5253 60.4622 26.5253C63.1396 26.5253 65.4188 26.5619 68.0598 26.892V26.9653ZM68.096 23.7016C66.4682 23.3349 64.0081 23.1149 61.0773 23.1149C52.5388 23.1149 48.5228 25.1318 48.5228 29.8257C48.5228 34.7029 51.2725 36.5731 57.6402 36.5731C63.031 36.5731 66.6853 35.2163 68.0236 31.8792H68.2407C68.2045 32.6859 68.1683 33.4927 68.1683 34.1161C68.1683 35.8396 68.4578 35.9864 69.8691 35.9864H76.5262C76.1644 34.9596 75.9472 32.0626 75.9472 29.5689C75.9472 26.892 76.0558 24.8751 76.0558 22.1615C76.0558 16.6241 72.7272 13.1037 62.3073 13.1037C57.821 13.1037 52.8282 13.8738 49.0293 15.0106C49.3911 16.5141 49.8977 19.5578 50.1509 21.538C53.4433 19.9979 58.1105 19.3378 61.7283 19.3378C66.7215 19.3378 68.096 20.4745 68.096 22.7848V23.7016Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M86.3622 28.5055C85.4576 28.6155 84.2278 28.6155 82.9613 28.6155C81.6224 28.6155 80.3927 28.5788 79.5604 28.4688C79.5604 28.7622 79.5242 29.0922 79.5242 29.3856C79.5242 33.9695 82.5271 36.6464 93.0917 36.6464C103.041 36.6464 106.261 34.0062 106.261 29.3489C106.261 24.9484 104.127 22.7848 94.6833 22.3081C87.3393 21.9781 86.6879 21.1713 86.6879 20.2545C86.6879 19.1911 87.6288 18.641 92.5489 18.641C97.6506 18.641 99.0251 19.3378 99.0251 20.8046V21.1346C99.7489 21.098 101.052 21.0613 102.39 21.0613C103.656 21.0613 105.031 21.098 105.827 21.1713C105.827 20.8413 105.863 20.5479 105.863 20.2912C105.863 14.9006 101.377 13.1404 92.6937 13.1404C82.9251 13.1404 79.6327 15.524 79.6327 20.1812C79.6327 24.3617 82.2738 26.9653 91.6443 27.3687C98.5547 27.5887 99.3146 28.3588 99.3146 29.4223C99.3146 30.559 98.1928 31.0725 93.345 31.0725C87.7735 31.0725 86.3622 30.3024 86.3622 28.7255V28.5055Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M118.203 9.17995C115.562 11.6369 110.822 14.0939 108.181 14.7539C108.217 16.1107 108.217 18.6044 108.217 19.9612L110.641 19.9979C110.605 22.6015 110.569 25.7552 110.569 27.8454C110.569 32.7226 113.137 36.3897 121.133 36.3897C124.498 36.3897 126.741 36.023 129.527 35.4363C129.237 33.6394 128.912 30.8891 128.804 28.7988C127.139 29.3489 125.041 29.6423 122.725 29.6423C119.505 29.6423 118.203 28.7622 118.203 26.2319C118.203 24.0316 118.203 21.9781 118.239 20.0712C122.364 20.1078 126.488 20.1812 128.912 20.2545C128.876 18.3477 128.948 15.5973 129.057 13.7638C125.547 13.8371 121.604 13.8738 118.348 13.8738C118.384 12.2603 118.42 10.7201 118.456 9.17995H118.203Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M139.812 18.861C139.848 16.9541 139.884 15.3406 139.92 13.7638H132.648C132.757 16.9175 132.757 20.1445 132.757 24.8751C132.757 29.6056 132.72 32.8693 132.648 35.9864H140.969C140.824 33.7861 140.788 30.0823 140.788 26.9286C140.788 21.9414 142.815 20.5112 147.409 20.5112C149.544 20.5112 151.063 20.768 152.402 21.2446C152.438 19.3745 152.8 15.744 153.017 14.1305C151.642 13.7271 150.123 13.4705 148.278 13.4705C144.334 13.4338 141.44 15.0473 140.101 18.8977L139.812 18.861Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M176.038 24.7284C176.038 28.7255 173.143 30.5957 168.584 30.5957C164.062 30.5957 161.167 28.8355 161.167 24.7284C161.167 20.6213 164.098 19.0811 168.584 19.0811C173.107 19.0811 176.038 20.7313 176.038 24.7284ZM183.599 24.5451C183.599 16.5875 177.376 13.0304 168.584 13.0304C159.757 13.0304 153.75 16.5875 153.75 24.5451C153.75 32.4659 159.359 36.7198 168.548 36.7198C177.81 36.7198 183.599 32.4659 183.599 24.5451Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_5336_22429"
|
||||
x1="10.1338"
|
||||
y1="48.1347"
|
||||
x2="31.1641"
|
||||
y2="38.1736"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#D83333" />
|
||||
<stop offset="1" stopColor="#F041FF" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5336_22429">
|
||||
<rect width="184" height="48.4533" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
href="/documentation/quickstarts/astro"
|
||||
/>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
|
||||
<Card
|
||||
icon={
|
||||
<svg
|
||||
width="160"
|
||||
height="55"
|
||||
viewBox="-30 6 220 55"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_5342_22458)">
|
||||
<path
|
||||
d="M181.319 7.73806L184 0.829078L183.889 0.386903L157.47 7.35116C160.288 3.26104 159.321 0 159.321 0C159.321 0 150.892 5.38901 144.508 5.25083C138.124 5.11265 136.079 3.39922 126.296 6.52208C116.513 9.64494 113.749 19.207 110.903 21.2797C108.084 23.3247 99.213 30.1508 99.213 30.1508L107.227 27.5807C107.227 27.5807 104.961 29.7363 100.291 36.0096V36.0372C101.037 37.0874 104.326 41.4263 107.587 40.4866C107.946 40.3761 108.36 40.1826 108.83 39.9615C110.295 40.7906 112.257 41.5921 114.385 41.8132C114.385 41.8132 112.948 40.155 111.732 38.2205L112.727 37.5849L112.561 37.6954L115.629 38.8008L115.297 35.9267H115.325L118.309 37.0321L117.95 34.4067L119.083 33.854L122.206 22.0258L135.14 13.1823L134.117 15.7801C131.492 22.2193 126.573 23.7393 126.573 23.7393L124.527 24.5407C122.98 26.337 122.344 26.7792 121.819 32.8591C123.063 32.5275 124.223 32.4722 125.301 32.7486C130.828 34.2409 132.735 40.9012 131.243 42.7528C130.884 43.195 129.999 43.9964 128.866 44.9084H126.628L126.6 46.7324L126.379 46.9258H124.085L124.058 48.6945L123.45 49.1367C121.322 49.1919 118.586 47.3127 118.586 47.3127C118.613 49.0261 120.023 51.6515 120.023 51.6515L120.272 51.5134L120.05 51.6792C120.05 51.6792 125.799 55.4929 129.419 54.0835C132.625 52.8123 140.943 46.2625 148.129 43.1673L169.85 37.419L172.725 30.0126L156.171 34.3791V27.6912L175.599 22.5786L178.473 15.1445L156.171 21.0309V14.3707L181.319 7.73806ZM141.883 21.86L147.051 20.5059L147.106 20.7546L145.503 24.9276L140.169 26.337L141.883 21.86ZM143.652 30.7864L138.318 32.1959L140.059 27.7188L145.199 26.3647L145.282 26.6134L143.652 30.7864ZM150.616 29.2941L145.282 30.7035L147.023 26.2265L152.163 24.8723L152.246 25.1211L150.616 29.2941Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M4.00721 16.7197L4.17302 15.863C4.64284 13.3758 5.58246 11.2478 7.32352 9.64495C8.59477 8.4566 10.474 7.73807 12.602 7.73807C13.9561 7.73807 14.9787 7.93152 15.6143 8.15261L14.426 12.2427C13.9285 12.0769 13.4587 11.994 12.7678 11.994C10.8609 11.994 9.75548 13.9838 9.39621 15.8078L9.2304 16.7197H13.2652L12.5467 20.4506H8.59477L5.3061 37.5296H0L3.26104 20.4506"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M21.4178 37.5296L21.5837 34.0475H21.4455C19.7873 36.8111 17.7975 37.8612 16.1394 37.8612C13.0994 37.8612 11.6071 35.2634 11.6071 31.6155C11.6071 25.3145 14.7576 16.3605 23.7116 16.3605C25.7843 16.3605 27.8846 16.7197 29.1559 17.2172L26.8621 28.7966C26.3647 31.2286 25.9778 35.2634 26.0054 37.5296H21.4178ZM23.3247 20.4506C22.9102 20.3677 22.5233 20.34 22.2193 20.34C18.7372 20.34 16.8579 27.2767 16.8026 30.3443C16.8026 32.1682 17.079 33.4947 18.3779 33.4947C19.8426 33.4947 21.1968 31.0904 21.9706 27.3043L23.3247 20.4506Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M29.7915 32.9697C30.6482 33.4395 31.6708 33.854 33.1631 33.8264C34.7107 33.7711 35.5398 32.8038 35.5398 31.4773C35.5398 30.3166 34.9871 29.5152 33.55 28.3821C31.7537 26.9174 30.897 25.0934 30.897 23.2971C30.897 19.4833 33.8816 16.3605 38.6074 16.3605C40.459 16.3605 41.7579 16.6921 42.5317 17.1343L41.371 21.0033C40.7906 20.6717 39.7957 20.3677 38.9114 20.3677C37.198 20.3677 36.1478 21.252 36.1478 22.6615C36.1478 23.7393 36.7558 24.3749 37.723 25.1763C40.0721 26.9727 40.8459 29.0177 40.8459 30.6206C40.8459 35.2358 37.6401 37.7783 32.8591 37.7783C30.9522 37.7783 29.2388 37.2256 28.5203 36.7282L29.7915 32.9697Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M53.3097 11.082L52.2319 16.7197H60.4121L59.7212 20.4506H51.5134L49.6894 30.1232C49.5512 30.8417 49.5236 31.4773 49.5236 31.8642C49.5236 33.1355 50.1592 33.6053 51.1265 33.6053C51.5134 33.6053 52.0661 33.6053 52.5635 33.5224L51.9279 37.5296C50.9606 37.7783 49.7723 37.8612 48.7498 37.8612C45.544 37.8612 43.9688 36.0925 43.9688 33.2736C43.9688 32.334 44.1346 31.1733 44.3557 30.1508L46.1796 20.4506H43.7753L44.4938 16.6921H46.9258L47.7549 12.4085L53.3097 11.082Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M54.581 37.5296L58.5882 16.7197H63.8943L59.9147 37.5296H54.581ZM59.5554 11.386C59.5554 9.83838 60.6609 8.09732 62.5401 8.09732C64.2812 8.09732 65.1103 9.42384 65.055 10.8609C64.9997 12.9889 63.5626 14.2049 61.9874 14.2049C60.274 14.2049 59.5278 12.9612 59.5554 11.386Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M67.2106 16.7197L67.3764 15.863C67.8462 13.3758 68.7858 11.2478 70.5269 9.64495C71.7981 8.4566 73.6774 7.73807 75.8054 7.73807C77.1595 7.73807 78.182 7.93152 78.8177 8.15261L77.6293 12.2427C77.1319 12.0769 76.6621 11.994 75.9712 11.994C74.0643 11.994 72.9589 13.9838 72.6272 15.8078L72.4338 16.7197H78.6242L77.9057 20.4506H71.7981L68.5371 37.5296H63.2034L66.492 20.4506"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M81.9405 16.7197L82.0787 26.7239C82.1063 28.6861 82.1616 30.1232 82.1063 31.6431H82.1892C82.6314 29.9574 83.046 28.5479 83.7645 26.337L86.915 16.6921H92.2764L85.4779 33.1078C83.4329 37.8612 81.0838 41.8684 78.5413 44.2175C77.2977 45.3506 75.8606 46.2625 74.9763 46.6218L72.8483 42.283C73.7603 41.8132 74.7552 41.2604 75.6119 40.5972C76.8002 39.6023 77.9333 38.4139 78.4584 37.2532C78.5413 36.9769 78.6242 36.7558 78.5689 36.3413L76.3581 16.6921H81.9405V16.7197Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5342_22458">
|
||||
<rect width="184" height="54.4151" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
href="/documentation/quickstarts/fastify"
|
||||
/>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
<Card
|
||||
icon={
|
||||
<svg width="160" height="38" viewBox="-26 -7 220 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_5291_19580)">
|
||||
<path d="M122.318 0.0154419H154.367V5.93096H141.653V37.0517H135.296V5.93096H122.318V0.0154419Z" fill="white"/>
|
||||
<path d="M69.608 0.0154419V5.93096H43.9181V15.4472H64.5779V21.3627H43.9181V31.1362H69.608V37.0517H37.5612L37.5586 0.0154419H69.608Z" fill="white"/>
|
||||
<path d="M85.6114 0.0308914H77.2921L107.09 37.0672H115.433L100.533 18.5618L115.41 0.0591477L107.09 0.0720044L96.3674 13.3889L85.6114 0.0308914Z" fill="white"/>
|
||||
<path d="M94.1482 26.4861L89.9821 21.3061L77.2683 37.0954H85.6114L94.1482 26.4861Z" fill="white"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M37.784 37.0517L7.94614 0H0V37.0363H6.35692V7.91648L29.7981 37.0517H37.784Z" fill="white"/>
|
||||
<path d="M155.796 36.8255C155.331 36.8255 154.935 36.6642 154.603 36.3417C154.271 36.0191 154.107 35.6286 154.112 35.166C154.107 34.7161 154.271 34.3298 154.603 34.0073C154.935 33.6847 155.331 33.5234 155.796 33.5234C156.244 33.5234 156.636 33.6847 156.967 34.0073C157.304 34.3298 157.471 34.7161 157.476 35.166C157.471 35.4716 157.394 35.7517 157.239 36.0021C157.079 36.2568 156.877 36.4563 156.623 36.6006C156.373 36.7491 156.097 36.8255 155.796 36.8255Z" fill="white"/>
|
||||
<path d="M166.646 21.2232H169.463V32.0761C169.459 33.0735 169.243 33.9266 168.821 34.6439C168.395 35.3612 167.805 35.9087 167.047 36.2949C166.293 36.6769 165.41 36.8722 164.406 36.8722C163.489 36.8722 162.666 36.7066 161.934 36.3841C161.202 36.0615 160.62 35.5777 160.194 34.941C159.763 34.3043 159.552 33.5107 159.552 32.5599H162.373C162.378 32.9759 162.472 33.3366 162.653 33.638C162.834 33.9393 163.084 34.1685 163.403 34.3298C163.725 34.4911 164.096 34.5717 164.514 34.5717C164.966 34.5717 165.354 34.4783 165.668 34.2873C165.982 34.1006 166.224 33.8205 166.392 33.447C166.555 33.0777 166.642 32.6193 166.646 32.0761V21.2232Z" fill="white"/>
|
||||
<path d="M181.054 25.4676C180.985 24.8097 180.683 24.2961 180.158 23.9311C179.628 23.5619 178.943 23.3793 178.103 23.3793C177.513 23.3793 177.005 23.4685 176.583 23.6425C176.161 23.8208 175.833 24.0584 175.609 24.3598C175.386 24.6611 175.274 25.0049 175.265 25.3912C175.265 25.7138 175.342 25.9939 175.493 26.2273C175.644 26.465 175.846 26.6645 176.109 26.8258C176.367 26.9913 176.656 27.1271 176.971 27.2375C177.289 27.3478 177.608 27.4412 177.927 27.5176L179.395 27.8784C179.986 28.0142 180.559 28.1967 181.105 28.4301C181.652 28.6594 182.148 28.9522 182.583 29.3045C183.018 29.6568 183.363 30.0812 183.617 30.5778C183.871 31.0744 184 31.6558 184 32.3265C184 33.2305 183.767 34.0242 183.298 34.7118C182.828 35.3951 182.152 35.9299 181.265 36.3162C180.382 36.6982 179.314 36.8934 178.056 36.8934C176.841 36.8934 175.782 36.7067 174.89 36.3332C173.994 35.9639 173.296 35.4206 172.792 34.7075C172.288 33.9945 172.017 33.1244 171.978 32.1015H174.77C174.808 32.6363 174.981 33.082 175.274 33.4427C175.571 33.7993 175.958 34.0624 176.432 34.2407C176.91 34.4147 177.444 34.5038 178.034 34.5038C178.65 34.5038 179.193 34.4104 179.662 34.2279C180.128 34.0454 180.494 33.7908 180.757 33.4597C181.024 33.1329 181.157 32.7467 181.162 32.3053C181.157 31.902 181.036 31.5667 180.804 31.3035C180.567 31.0404 180.24 30.8197 179.822 30.6415C179.4 30.4632 178.909 30.3019 178.349 30.1618L176.566 29.7119C175.278 29.3851 174.257 28.8886 173.512 28.2222C172.762 27.5558 172.392 26.673 172.392 25.5652C172.392 24.6569 172.641 23.859 173.146 23.1756C173.645 22.4923 174.33 21.9618 175.196 21.584C176.066 21.202 177.048 21.0152 178.142 21.0152C179.253 21.0152 180.227 21.202 181.067 21.584C181.906 21.9618 182.566 22.4881 183.044 23.1586C183.522 23.8293 183.772 24.5975 183.785 25.4676H181.054Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5291_19580">
|
||||
<rect width="184" height="37.3604" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>}
|
||||
href="/documentation/quickstarts/nextjs"
|
||||
|
||||
/>
|
||||
@@ -0,0 +1,28 @@
|
||||
<Card
|
||||
icon={
|
||||
<svg
|
||||
width="150"
|
||||
height="46"
|
||||
viewBox="-30 0 220 58"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_5336_22448)">
|
||||
<path
|
||||
d="M38.7097 46H64.3598C65.1746 46 65.9748 45.7875 66.6805 45.3838C67.3846 44.9814 67.9704 44.4006 68.379 43.7C68.7859 43.0017 69.0002 42.208 69 41.3998C68.9994 40.5916 68.7844 39.7981 68.3769 39.1002L51.1511 9.52867C50.7425 8.82816 50.1568 8.24752 49.4528 7.84507C48.7462 7.44109 47.9464 7.22869 47.1325 7.2289C46.3181 7.2289 45.5179 7.44165 44.8125 7.8453C44.1084 8.24768 43.5227 8.82834 43.1142 9.5289L38.7097 17.095L30.0978 2.29954C29.6891 1.59892 29.1031 1.01826 28.3988 0.61594C27.6921 0.212089 26.8922 -0.000228035 26.0783 1.8379e-07C25.2637 1.8379e-07 24.4633 0.21252 23.7576 0.61617C23.0534 1.01847 22.4674 1.59904 22.0586 2.29954L0.623071 39.1C0.215509 39.7979 0.000499188 40.5915 7.38785e-07 41.3998C-0.000459261 42.2073 0.213901 43.0006 0.621001 43.7C1.0296 44.4006 1.61541 44.9813 2.31955 45.3838C3.02626 45.7878 3.82623 46.0002 4.64025 46H20.7412C27.1207 46 31.8253 43.2228 35.0624 37.8044L42.9217 24.3144L47.1314 17.095L59.7653 38.7805H42.9217L38.7097 46ZM20.4787 38.7732L9.24232 38.7706L26.0857 9.85826L34.4901 24.3144L28.8629 33.9768C26.7131 37.4923 24.271 38.7732 20.4787 38.7732Z"
|
||||
fill="#00DC82"
|
||||
/>
|
||||
<path
|
||||
d="M86.71 46C86.954 46 87.188 45.9031 87.3605 45.7305C87.5331 45.558 87.63 45.324 87.63 45.08V23.69C87.63 23.69 88.8361 25.5958 91.08 29.44L99.9826 44.841C100.394 45.5584 101.152 46 101.972 46H108.1V11.5H101.89C101.646 11.5 101.412 11.5969 101.239 11.7695C101.067 11.942 100.97 12.176 100.97 12.42V34.04L97.06 27.14L88.6448 12.6546C88.2326 11.9398 87.4761 11.5 86.6578 11.5H80.5V46H86.71ZM155.527 33.0871L163.427 21.16H157.509C157.128 21.1607 156.754 21.2561 156.419 21.4377C156.084 21.6193 155.8 21.8813 155.592 22.2001L151.974 27.715L148.356 22.2001C148.148 21.8813 147.864 21.6193 147.529 21.4377C147.195 21.2561 146.82 21.1607 146.439 21.16H140.568L148.468 33.0379L139.961 46H145.703C146.08 45.9993 146.452 45.9054 146.785 45.7265C147.117 45.5477 147.401 45.2894 147.609 44.9747L152.021 38.3608L156.386 44.9664C156.595 45.2834 156.878 45.5438 157.212 45.7242C157.546 45.9046 157.919 45.9993 158.298 46H163.988L155.527 33.0871ZM166.658 21.16H171.192V13.8163H177.689V21.16H184V26.8771H177.689V36.685C177.689 39.1 178.905 40.2337 181.008 40.2337H184V46H180.12C174.698 46 171.192 42.7471 171.192 36.9808V26.8771H166.658V21.16ZM135.7 21.16H132.25C131.448 21.16 130.82 21.1934 130.295 21.7403C129.77 22.2569 129.72 22.5752 129.72 23.3655V35.4372C129.72 37.2906 129.634 38.4185 128.8 39.33C127.966 40.2111 126.818 40.48 125.12 40.48C123.453 40.48 122.274 40.2111 121.44 39.33C120.606 38.4185 120.52 37.2906 120.52 35.4372V23.3655C120.52 22.5754 120.47 22.2569 119.945 21.7403C119.42 21.1934 118.792 21.16 117.99 21.16H114.54V35.5171C114.54 38.738 115.435 41.2903 117.225 43.1742C119.047 45.0582 121.694 46 125.12 46C128.546 46 131.147 45.0582 132.968 43.1742C134.789 41.2903 135.7 38.738 135.7 35.5171V21.16Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5336_22448">
|
||||
<rect width="184" height="46" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
href="/documentation/quickstarts/nuxt"
|
||||
/>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,206 @@
|
||||
<Card
|
||||
icon={
|
||||
<svg
|
||||
width="180"
|
||||
height="38"
|
||||
viewBox="22 0 184 58"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g filter="url(#filter0_d_5334_22407)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M47.4777 45.058C47.8835 50.3106 47.8835 52.7727 47.8835 55.4603H35.8237C35.8237 54.8748 35.8341 54.3393 35.8445 53.7962C35.8771 52.1081 35.9111 50.3479 35.6397 46.7928C35.2811 41.5885 33.0571 40.4319 28.9676 40.4319H25.3444H10V30.9627H29.5415C34.7071 30.9627 37.2898 29.379 37.2898 25.1865C37.2898 21.5001 34.7071 19.2659 29.5415 19.2659H10V10H31.6937C43.3882 10 49.1997 15.5659 49.1997 24.457C49.1997 31.1071 45.1102 35.4441 39.5857 36.167C44.2492 37.1068 46.9754 39.7813 47.4777 45.058Z"
|
||||
fill="#E5F3FF"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M10 55.4604V48.4012H22.7516C24.8815 48.4012 25.3439 49.9932 25.3439 50.9423V55.4604H10Z"
|
||||
fill="#E5F3FF"
|
||||
/>
|
||||
<g filter="url(#filter1_d_5334_22407)">
|
||||
<path
|
||||
d="M192.998 23.7133H181.045L175.606 31.3573L170.309 23.7133H157.498L169.021 39.5059L156.496 55.8753H168.448L174.819 47.1497L181.188 55.8753H194L181.403 39.0011L192.998 23.7133Z"
|
||||
fill="#FFF1F1"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d_5334_22407)">
|
||||
<path
|
||||
d="M117.711 29.0434C116.351 25.2935 113.416 22.6975 107.762 22.6975C102.966 22.6975 99.5309 24.861 97.8132 28.3943V23.5629H86.2185V55.7246H97.8132V39.9322C97.8132 35.1008 99.1731 31.9279 102.966 31.9279C106.474 31.9279 107.333 34.2354 107.333 38.6343V55.7246H118.927V39.9322C118.927 35.1008 120.215 31.9279 124.08 31.9279C127.588 31.9279 128.375 34.2354 128.375 38.6343V55.7246H139.97V35.5335C139.97 28.827 137.393 22.6975 128.59 22.6975C123.222 22.6975 119.428 25.4377 117.711 29.0434Z"
|
||||
fill="#FFFAEA"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter3_d_5334_22407)">
|
||||
<path
|
||||
d="M74.1446 43.2432C73.071 45.7671 71.067 46.8487 67.9178 46.8487C64.4107 46.8487 61.5477 44.9738 61.2614 41.0076H83.6637V37.7628C83.6637 29.0372 78.0096 21.6817 67.345 21.6817C57.3964 21.6817 49.9529 28.9651 49.9529 39.1328C49.9529 49.3725 57.2534 55.5742 67.4883 55.5742C75.934 55.5742 81.803 51.464 83.4491 44.1084L74.1446 43.2432ZM61.4047 35.3829C61.8339 32.3542 63.4803 30.0466 67.202 30.0466C70.6375 30.0466 72.4983 32.4984 72.6415 35.3829H61.4047Z"
|
||||
fill="#F1FFF0"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter4_d_5334_22407)">
|
||||
<path
|
||||
d="M143.762 23.7765V55.9383H155.357V23.7765H143.762ZM143.691 20.7478H155.429V10.5078H143.691V20.7478Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_5334_22407"
|
||||
x="0"
|
||||
y="0"
|
||||
width="59.1997"
|
||||
height="65.4604"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="5" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.203922 0 0 0 0 0.45098 0 0 0 0 0.74902 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5334_22407" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5334_22407"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_d_5334_22407"
|
||||
x="146.496"
|
||||
y="13.7133"
|
||||
width="57.5044"
|
||||
height="52.162"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="5" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.882353 0 0 0 0 0.180392 0 0 0 0 0.227451 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5334_22407" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5334_22407"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_d_5334_22407"
|
||||
x="78.2185"
|
||||
y="14.6975"
|
||||
width="69.7512"
|
||||
height="49.0271"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="4" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.937255 0 0 0 0 0.72549 0 0 0 0 0.117647 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5334_22407" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5334_22407"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter3_d_5334_22407"
|
||||
x="41.9529"
|
||||
y="13.6817"
|
||||
width="49.7108"
|
||||
height="49.8926"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="4" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.384314 0 0 0 0 0.788235 0 0 0 0 0.372549 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5334_22407" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5334_22407"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter4_d_5334_22407"
|
||||
x="133.691"
|
||||
y="0.507791"
|
||||
width="31.738"
|
||||
height="65.4305"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset />
|
||||
<feGaussianBlur stdDeviation="5" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.596078 0 0 0 0 0.192157 0 0 0 0 0.556863 0 0 0 1 0"
|
||||
/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5334_22407" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5334_22407"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
href="/documentation/quickstarts/remix"
|
||||
/>
|
||||
@@ -0,0 +1,132 @@
|
||||
<Card
|
||||
icon={
|
||||
<svg
|
||||
width="307"
|
||||
height="36"
|
||||
viewBox="0 -5 307 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_5373_35)">
|
||||
<g clipPath="url(#clip1_5373_35)">
|
||||
<path
|
||||
d="M274.818 6.00787H291.539V9.01627H284.906V24.843H281.589V9.01627H274.818V6.00787Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M247.317 6.00787V9.01627H233.914V13.8559H244.693V16.8642H233.914V21.8346H247.317V24.843H230.597L230.596 6.00787H247.317Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M255.667 6.01569H251.326L266.873 24.8508H271.226L263.452 15.4397L271.214 6.03006L266.873 6.03659L261.279 12.809L255.667 6.01569Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M260.121 19.4698L257.947 16.8354L251.314 24.8653H255.667L260.121 19.4698Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M230.713 24.843L215.146 6H211V24.8351H214.317V10.026L226.547 24.843H230.713Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M292.285 24.7279C292.042 24.7279 291.835 24.6459 291.662 24.4819C291.489 24.3178 291.404 24.1192 291.406 23.8839C291.404 23.6551 291.489 23.4587 291.662 23.2947C291.835 23.1306 292.042 23.0486 292.285 23.0486C292.519 23.0486 292.723 23.1306 292.896 23.2947C293.071 23.4587 293.159 23.6551 293.161 23.8839C293.159 24.0393 293.118 24.1818 293.038 24.3091C292.955 24.4387 292.849 24.5401 292.716 24.6135C292.586 24.6891 292.442 24.7279 292.285 24.7279Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M297.946 16.7932H299.416V22.3125C299.413 22.8198 299.301 23.2537 299.081 23.6185C298.858 23.9832 298.55 24.2617 298.155 24.4581C297.762 24.6523 297.301 24.7517 296.777 24.7517C296.298 24.7517 295.869 24.6675 295.487 24.5034C295.105 24.3394 294.802 24.0933 294.579 23.7695C294.355 23.4458 294.244 23.0421 294.244 22.5586H295.717C295.719 22.7702 295.768 22.9536 295.863 23.1069C295.957 23.2601 296.087 23.3767 296.254 23.4587C296.422 23.5407 296.615 23.5817 296.833 23.5817C297.069 23.5817 297.272 23.5342 297.436 23.4371C297.6 23.3421 297.726 23.1997 297.813 23.0097C297.899 22.822 297.944 22.5888 297.946 22.3125V16.7932Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M305.463 18.9518C305.427 18.6172 305.27 18.356 304.995 18.1704C304.719 17.9826 304.362 17.8898 303.923 17.8898C303.616 17.8898 303.351 17.9351 303.13 18.0236C302.91 18.1143 302.739 18.2351 302.622 18.3884C302.506 18.5417 302.447 18.7165 302.442 18.9129C302.442 19.077 302.483 19.2194 302.562 19.3382C302.64 19.459 302.746 19.5605 302.883 19.6425C303.018 19.7267 303.168 19.7958 303.333 19.8519C303.499 19.908 303.665 19.9555 303.831 19.9944L304.598 20.1778C304.906 20.2469 305.205 20.3397 305.49 20.4584C305.775 20.575 306.034 20.7239 306.261 20.9031C306.488 21.0822 306.667 21.2981 306.8 21.5506C306.933 21.8032 307 22.0989 307 22.4399C307 22.8997 306.879 23.3033 306.634 23.653C306.389 24.0005 306.036 24.2725 305.573 24.4689C305.112 24.6632 304.555 24.7625 303.899 24.7625C303.265 24.7625 302.712 24.6675 302.247 24.4776C301.78 24.2898 301.415 24.0135 301.153 23.6509C300.89 23.2882 300.748 22.8457 300.728 22.3255H302.184C302.204 22.5975 302.294 22.8242 302.447 23.0076C302.602 23.1889 302.804 23.3228 303.052 23.4134C303.301 23.5019 303.58 23.5473 303.888 23.5473C304.209 23.5473 304.492 23.4998 304.737 23.4069C304.98 23.3141 305.171 23.1846 305.308 23.0163C305.447 22.8501 305.517 22.6536 305.519 22.4292C305.517 22.2241 305.454 22.0536 305.332 21.9197C305.209 21.7859 305.038 21.6737 304.82 21.583C304.6 21.4924 304.344 21.4103 304.052 21.3391L303.121 21.1103C302.449 20.9441 301.917 20.6916 301.528 20.3527C301.137 20.0138 300.943 19.5648 300.943 19.0014C300.943 18.5395 301.074 18.1337 301.337 17.7862C301.597 17.4387 301.955 17.1689 302.407 16.9768C302.861 16.7825 303.373 16.6875 303.944 16.6875C304.523 16.6875 305.031 16.7825 305.47 16.9768C305.908 17.1689 306.252 17.4365 306.501 17.7776C306.75 18.1186 306.881 18.5093 306.888 18.9518H305.463Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M47.9467 21.1428C48.1364 22.9167 49.75 25.9577 54.4327 25.9577C58.5139 25.9577 60.4758 23.3602 60.4758 20.8261C60.4758 18.5454 58.9253 16.6765 55.8565 16.0429L53.6415 15.5678C52.7874 15.4094 52.218 14.9342 52.218 14.174C52.218 13.287 53.1038 12.6219 54.2113 12.6219C55.9829 12.6219 56.6473 13.7939 56.774 14.7125L60.2858 13.9206C60.0961 12.2417 58.6089 9.42255 54.1796 9.42255C50.8258 9.42255 48.3581 11.7349 48.3581 14.5224C48.3581 16.7081 49.7183 18.5137 52.7241 19.1789L54.7807 19.6541C55.9829 19.9074 56.4576 20.4777 56.4576 21.1745C56.4576 21.9981 55.7932 22.7267 54.401 22.7267C52.566 22.7267 51.6483 21.5863 51.5536 20.3509L47.9467 21.1428Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M73.9233 25.4825H77.9416C77.8782 24.944 77.7832 23.867 77.7832 22.6633V9.89764H73.575V18.9571C73.575 20.7627 72.4995 22.0298 70.6329 22.0298C68.6713 22.0298 67.7852 20.636 67.7852 18.8938V9.89764H63.5772V19.7491C63.5772 23.1385 65.7285 25.8943 69.4937 25.8943C71.1386 25.8943 72.9429 25.2608 73.797 23.8037C73.797 24.4372 73.86 25.1657 73.9233 25.4825Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M86.4819 31.5011V23.9621C87.241 25.0074 88.8232 25.8627 90.9749 25.8627C95.3725 25.8627 98.3146 22.3782 98.3146 17.6584C98.3146 13.0337 95.6889 9.54926 91.1329 9.54926C88.7919 9.54926 87.0516 10.5945 86.3555 11.7983V9.89768H82.2743V31.5011H86.4819ZM94.17 17.6901C94.17 20.4777 92.4617 22.0932 90.3101 22.0932C88.1591 22.0932 86.4189 20.446 86.4189 17.6901C86.4189 14.9343 88.1591 13.3188 90.3101 13.3188C92.4617 13.3188 94.17 14.9343 94.17 17.6901Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M100.698 21.2379C100.698 23.677 102.722 25.926 106.045 25.926C108.354 25.926 109.841 24.849 110.632 23.6136C110.632 24.2155 110.695 25.0707 110.79 25.4825H114.65C114.555 24.944 114.461 23.8354 114.461 23.0118V15.346C114.461 12.21 112.625 9.42255 107.69 9.42255C103.513 9.42255 101.267 12.115 101.014 14.5541L104.747 15.346C104.874 13.9839 105.886 12.8119 107.721 12.8119C109.493 12.8119 110.347 13.7305 110.347 14.8392C110.347 15.3777 110.063 15.8212 109.177 15.9479L105.349 16.5181C102.754 16.8982 100.698 18.4503 100.698 21.2379ZM106.93 22.79C105.57 22.79 104.905 21.9031 104.905 20.9845C104.905 19.7808 105.759 19.1789 106.836 19.0205L110.347 18.482V19.1789C110.347 21.9348 108.702 22.79 106.93 22.79Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M122.784 25.4826V23.582C123.606 24.9124 125.251 25.8627 127.403 25.8627C131.833 25.8627 134.743 22.3466 134.743 17.6268C134.743 13.002 132.117 9.4859 127.561 9.4859C125.251 9.4859 123.543 10.4995 122.847 11.6082V2.54871H118.702V25.4826H122.784ZM130.535 17.6584C130.535 20.5093 128.827 22.0932 126.675 22.0932C124.555 22.0932 122.784 20.4777 122.784 17.6584C122.784 14.8075 124.555 13.2554 126.675 13.2554C128.827 13.2554 130.535 14.8075 130.535 17.6584Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M137.126 21.2379C137.126 23.677 139.151 25.926 142.473 25.926C144.782 25.926 146.27 24.849 147.06 23.6136C147.06 24.2155 147.124 25.0707 147.218 25.4825H151.078C150.984 24.944 150.889 23.8354 150.889 23.0118V15.346C150.889 12.21 149.054 9.42255 144.118 9.42255C139.942 9.42255 137.695 12.115 137.442 14.5541L141.175 15.346C141.302 13.9839 142.315 12.8119 144.149 12.8119C145.921 12.8119 146.776 13.7305 146.776 14.8392C146.776 15.3777 146.491 15.8212 145.605 15.9479L141.777 16.5181C139.183 16.8982 137.126 18.4503 137.126 21.2379ZM143.359 22.79C141.998 22.79 141.334 21.9031 141.334 20.9845C141.334 19.7808 142.188 19.1789 143.264 19.0205L146.776 18.482V19.1789C146.776 21.9348 145.13 22.79 143.359 22.79Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M153.802 21.1428C153.991 22.9167 155.605 25.9577 160.287 25.9577C164.369 25.9577 166.33 23.3602 166.33 20.8261C166.33 18.5454 164.78 16.6765 161.711 16.0429L159.496 15.5678C158.642 15.4094 158.073 14.9342 158.073 14.174C158.073 13.287 158.958 12.6219 160.066 12.6219C161.838 12.6219 162.502 13.7939 162.629 14.7125L166.141 13.9206C165.951 12.2417 164.464 9.42255 160.034 9.42255C156.68 9.42255 154.213 11.7349 154.213 14.5224C154.213 16.7081 155.573 18.5137 158.579 19.1789L160.635 19.6541C161.838 19.9074 162.312 20.4777 162.312 21.1745C162.312 21.9981 161.648 22.7267 160.256 22.7267C158.421 22.7267 157.503 21.5863 157.408 20.3509L153.802 21.1428Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M172.721 15.9162C172.816 14.4908 174.018 12.8436 176.201 12.8436C178.606 12.8436 179.619 14.3641 179.682 15.9162H172.721ZM180.093 19.9708C179.587 21.3646 178.511 22.3466 176.549 22.3466C174.461 22.3466 172.721 20.8578 172.627 18.7988H183.763C183.763 18.7354 183.826 18.1019 183.826 17.5C183.826 12.4951 180.947 9.42255 176.138 9.42255C172.152 9.42255 168.481 12.6535 168.481 17.6268C168.481 22.885 172.246 25.9577 176.518 25.9577C180.346 25.9577 182.814 23.7086 183.605 21.0161L180.093 19.9708Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M20.1759 34.9264C19.2703 36.0669 17.4341 35.442 17.4123 33.9859L17.0932 12.6877H31.4141C34.008 12.6877 35.4547 15.6837 33.8417 17.7151L20.1759 34.9264Z"
|
||||
fill="url(#paint0_linear_5373_35)"
|
||||
/>
|
||||
<path
|
||||
d="M20.1759 34.9264C19.2703 36.0669 17.4341 35.442 17.4123 33.9859L17.0932 12.6877H31.4141C34.008 12.6877 35.4547 15.6837 33.8417 17.7151L20.1759 34.9264Z"
|
||||
fill="url(#paint1_linear_5373_35)"
|
||||
fillOpacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M14.3517 0.6559C15.2573 -0.484647 17.0935 0.140291 17.1153 1.59648L17.2551 22.8946H3.11348C0.519504 22.8946 -0.927202 19.8986 0.685807 17.8672L14.3517 0.6559Z"
|
||||
fill="#3ECF8E"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M198 13H197V16H194V17H197V20H198V17H201V16H198V13Z"
|
||||
fill="#D9D9D9"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_5373_35"
|
||||
x1="17.0932"
|
||||
y1="17.41"
|
||||
x2="29.8211"
|
||||
y2="22.7481"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#249361" />
|
||||
<stop offset="1" stopColor="#3ECF8E" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_5373_35"
|
||||
x1="11.4504"
|
||||
y1="9.68392"
|
||||
x2="17.255"
|
||||
y2="20.6107"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5373_35">
|
||||
<rect width="307" height="36" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_5373_35">
|
||||
<rect width="96" height="19" fill="white" transform="translate(211 6)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
href="/documentation/quickstarts/supabase"
|
||||
/>
|
||||
@@ -0,0 +1,36 @@
|
||||
<Card
|
||||
icon={
|
||||
<svg
|
||||
width="234"
|
||||
height="40"
|
||||
viewBox="10 -6 240 44"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_5341_22451)">
|
||||
<path
|
||||
d="M153.928 25.3304L149.002 16.1776L145.901 19.929V25.3304H143.3V7.82486H145.901V16.3027L152.653 7.82486H155.654L150.753 13.977L156.98 25.3304H153.928ZM162.606 25.3304V7.82486H165.206V25.3304H162.606ZM178.761 10.3257V25.3304H176.16V10.3257H171.208V7.82486H183.712V10.3257H178.761Z"
|
||||
fill="#8D8D93"
|
||||
/>
|
||||
<path
|
||||
d="M43.8554 25.6303C42.4293 25.6662 41.0258 25.2695 39.8294 24.4926C38.7117 23.7554 37.8816 22.6561 37.4785 21.3792L39.9292 20.4789C40.2691 21.2628 40.8241 21.9343 41.53 22.4157C42.2466 22.8959 43.0933 23.1447 43.9558 23.1285C44.7848 23.1719 45.603 22.9242 46.2688 22.4283C46.5585 22.1851 46.7873 21.8776 46.9371 21.5302C47.0869 21.1829 47.1535 20.8054 47.1316 20.4278C47.1349 20.076 47.0534 19.7285 46.8939 19.4148C46.7552 19.1342 46.5738 18.8768 46.3563 18.6518C46.0811 18.4012 45.7691 18.1946 45.431 18.0391C45.0139 17.8311 44.6679 17.6727 44.393 17.5639C44.1181 17.4552 43.7138 17.3093 43.1801 17.1264C42.5134 16.893 42.0133 16.7097 41.68 16.5764C41.2284 16.3842 40.7898 16.1628 40.367 15.9135C39.912 15.6721 39.4983 15.3598 39.1415 14.9882C38.8329 14.6257 38.5839 14.2162 38.4041 13.7754C38.0584 12.9348 37.9858 12.0069 38.1964 11.1229C38.4071 10.2388 38.8904 9.44335 39.5779 8.84903C40.5782 7.96552 41.9368 7.52366 43.6538 7.52347C45.0876 7.52347 46.2672 7.84021 47.1926 8.47369C48.0908 9.07097 48.7503 9.96521 49.0555 10.9998L46.655 11.7998C46.4138 11.258 46.0065 10.807 45.492 10.5121C44.8698 10.1667 44.1651 9.99797 43.4539 10.024C42.7565 9.98411 42.0657 10.1777 41.4907 10.5743C41.2559 10.7563 41.0685 10.9924 40.9445 11.2623C40.8204 11.5323 40.7634 11.8282 40.7782 12.1249C40.7802 12.3524 40.8303 12.5769 40.9251 12.7837C41.0198 12.9906 41.1572 13.1751 41.3281 13.3252C41.6604 13.6524 42.0504 13.9153 42.4785 14.1005C42.8791 14.2672 43.4878 14.4922 44.3044 14.7755C44.8041 14.9592 45.1749 15.0967 45.4169 15.1881C45.6589 15.2794 46.0132 15.4336 46.4798 15.6506C46.8539 15.816 47.2136 16.0123 47.555 16.2375C47.8608 16.4574 48.1532 16.6954 48.4304 16.9503C48.732 17.2117 48.9896 17.5199 49.1934 17.863C49.3877 18.2188 49.5387 18.5965 49.6433 18.9881C49.771 19.4482 49.8342 19.9239 49.8311 20.4014C49.8311 22.0354 49.2726 23.3149 48.1555 24.2399C47.0384 25.1649 45.605 25.6283 43.8554 25.6303ZM59.8851 25.3304L53.8832 7.82485H56.6843L60.6604 20.0287C60.8801 20.6851 61.0638 21.3531 61.2107 22.0295C61.357 21.3529 61.5407 20.685 61.7609 20.0287L65.6868 7.82485H68.4627L62.4858 25.3304H59.8851ZM73.764 25.3304V7.82485H84.6174V10.2758H76.365V15.0772H81.6916V17.5281H76.365V22.8794H85.1688V25.3304H73.764ZM91.7943 25.3304V7.82485H94.3955V22.8295H102.948V25.3304H91.7943ZM112.75 10.3257V25.3304H110.149V10.3257H105.198V7.82485H117.702V10.3257H112.75ZM123.078 25.3304V7.82485H133.931V10.2758H125.679V15.0772H131.006V17.5281H125.679V22.8794H134.483V25.3304H123.078Z"
|
||||
fill="#4A4A55"
|
||||
/>
|
||||
<path
|
||||
d="M25.615 4.49554C22.563 0.127376 16.5351 -1.16825 12.1769 1.60935L4.52282 6.48785C3.48875 7.13833 2.6018 7.99743 1.91867 9.01023C1.23553 10.023 0.771258 11.1672 0.555527 12.3697C0.190411 14.395 0.51127 16.4843 1.46731 18.3067C0.812002 19.3005 0.365144 20.417 0.153779 21.5885C-0.0643107 22.8151 -0.0352588 24.073 0.239231 25.2882C0.51372 26.5035 1.02812 27.6517 1.75226 28.6656C4.80425 33.0343 10.8325 34.3294 15.1901 31.5518L22.8445 26.6733C23.8786 26.0228 24.7655 25.1637 25.4487 24.1509C26.1318 23.1381 26.5961 21.9939 26.8118 20.7914C27.1767 18.766 26.856 16.6768 25.9003 14.8541C26.5553 13.8603 27.0019 12.744 27.2132 11.5726C27.4315 10.346 27.4025 9.08811 27.128 7.87282C26.8535 6.65752 26.3393 5.5093 25.615 4.49554Z"
|
||||
fill="#FF3E00"
|
||||
/>
|
||||
<path
|
||||
d="M11.4291 29.0767C10.2246 29.3898 8.95296 29.3257 7.78598 28.8932C6.619 28.4606 5.61287 27.6803 4.90345 26.6577C4.46805 26.0481 4.15874 25.3578 3.99365 24.6271C3.82857 23.8964 3.81103 23.1401 3.94207 22.4025C3.98642 22.1604 4.04751 21.9216 4.1249 21.6879L4.26898 21.2478L4.66134 21.5356C5.56664 22.2013 6.57896 22.7074 7.65464 23.0323L7.93901 23.1186L7.91289 23.4026C7.87805 23.8066 7.98742 24.2097 8.22161 24.5407C8.43522 24.849 8.73834 25.0842 9.08998 25.2146C9.44162 25.345 9.82483 25.3644 10.1878 25.2699C10.3539 25.2258 10.5122 25.1565 10.6573 25.0645L18.312 20.1845C18.4992 20.0666 18.6599 19.9109 18.7837 19.7275C18.9075 19.544 18.9917 19.3368 19.0309 19.119C19.0702 18.8966 19.0648 18.6687 19.015 18.4485C18.9652 18.2282 18.8721 18.0201 18.741 17.8363C18.5274 17.528 18.2243 17.2928 17.8727 17.1622C17.521 17.0317 17.1379 17.0122 16.7748 17.1064C16.6087 17.1505 16.4504 17.2198 16.3053 17.3119L13.3842 19.1739C12.9035 19.4793 12.379 19.7095 11.8288 19.8565C10.6243 20.1696 9.35266 20.1055 8.18568 19.6729C7.01869 19.2403 6.01256 18.4601 5.30314 17.4375C4.86762 16.8279 4.55821 16.1376 4.39307 15.4069C4.22793 14.6762 4.21039 13.9198 4.34147 13.1823C4.47158 12.4591 4.75119 11.7711 5.16242 11.1622C5.57365 10.5533 6.10744 10.0369 6.72965 9.64607L14.3834 4.76787C14.8639 4.46198 15.3885 4.23149 15.9388 4.0844C17.1433 3.77125 18.4149 3.8353 19.5819 4.26789C20.7489 4.70048 21.755 5.48077 22.4644 6.5034C22.8999 7.11296 23.2093 7.80332 23.3744 8.53401C23.5395 9.2647 23.5571 10.021 23.4261 10.7586C23.3812 11.0011 23.3197 11.2402 23.2421 11.4743L23.098 11.9145L22.706 11.6272C21.8007 10.9615 20.7884 10.4553 19.7127 10.1306L19.428 10.0443L19.4544 9.76023C19.4889 9.35635 19.3795 8.95328 19.1457 8.62219C18.9322 8.31419 18.6293 8.07913 18.278 7.94872C17.9266 7.81831 17.5437 7.79884 17.181 7.89294C17.0149 7.93707 16.8566 8.00635 16.7114 8.09836L9.05592 12.9766C8.86868 13.0944 8.70806 13.25 8.58432 13.4335C8.46058 13.6169 8.37643 13.8241 8.33723 14.0418C8.29778 14.2642 8.30305 14.4922 8.35274 14.7125C8.40242 14.9328 8.49551 15.1409 8.62659 15.3248C8.84026 15.6331 9.14336 15.8683 9.49496 15.9988C9.84656 16.1293 10.2297 16.1488 10.5928 16.0547C10.7588 16.0104 10.9171 15.9411 11.0623 15.8493L13.9828 13.9881C14.4633 13.6822 14.9878 13.4518 15.5382 13.3049C16.7424 12.9919 18.0136 13.0559 19.1803 13.4882C20.347 13.9205 21.3529 14.7004 22.0624 15.7225C22.4979 16.332 22.8073 17.0224 22.9725 17.7531C23.1376 18.4838 23.1552 19.2401 23.0241 19.9777C22.8943 20.7008 22.615 21.3889 22.2042 21.998C21.7934 22.607 21.26 23.1237 20.6382 23.515L12.9845 28.3932C12.504 28.6992 11.9795 28.9297 11.4291 29.0767Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5341_22451">
|
||||
<rect width="184" height="33.1611" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
href="/documentation/quickstarts/sveltekit"
|
||||
/>
|
||||
@@ -1 +1,193 @@
|
||||
We're in the process of building support for the Express framework. You can follow along with progress or contribute via [this GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues).
|
||||
## Installing Required Packages
|
||||
|
||||
Start by installing the necessary packages in your Express.js project directory. You can use npm, pnpm, or yarn as your package manager.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install @trigger.dev/sdk @trigger.dev/express
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk @trigger.dev/express
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk @trigger.dev/express
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<Note>Ensure that you execute this command within a Express project.</Note>
|
||||
|
||||
## Obtaining the Development Server API Key
|
||||
|
||||
To locate your development Server API key, login to the [Trigger.dev
|
||||
dashboard](https://cloud.trigger.dev) and select the Project you want to
|
||||
connect to. Then click on the Environments & API Keys tab in the left menu.
|
||||
You can copy your development Server API Key from the field at the top of this page.
|
||||
(Your development key will start with `tr_dev_`).
|
||||
|
||||
## Adding Environment Variables
|
||||
|
||||
Create a `.env` file at the root of your project and include your Trigger API key and URL like this:
|
||||
|
||||
```bash
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this is only necessary if you are self-hosting
|
||||
```
|
||||
|
||||
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
|
||||
|
||||
## Configuring the Trigger Client
|
||||
|
||||
Create a file for your Trigger client, in this case we create it at `<root>/trigger.(ts/js)`
|
||||
|
||||
```ts trigger.(ts/js)
|
||||
import { TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY!,
|
||||
apiUrl: process.env.TRIGGER_API_URL!,
|
||||
});
|
||||
```
|
||||
|
||||
Replace **"my-app"** with an appropriate identifier for your project.
|
||||
|
||||
## Adding the API endpoint
|
||||
|
||||
There are a few different options depending on how your Express project is configured.
|
||||
|
||||
- App middleware
|
||||
- Entire app for Trigger.dev (only relevant if it's the only thing your project is for)
|
||||
|
||||
Select the appropriate code example from below:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```typescript app middleware
|
||||
//import the client from the other file
|
||||
import { client } from "./trigger";
|
||||
import { createMiddleware } from "@trigger.dev/express";
|
||||
|
||||
//import your job files
|
||||
import "./jobs/example";
|
||||
|
||||
//..your existing Express code
|
||||
const app: Express = express();
|
||||
|
||||
//add the middleware
|
||||
app.use(createMiddleware(client));
|
||||
|
||||
//..the rest of your Express code
|
||||
```
|
||||
|
||||
```typescript entire app
|
||||
//if the entire app is just for Trigger.dev
|
||||
import { client } from "./trigger";
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
|
||||
//import your job files
|
||||
import "./jobs/example";
|
||||
|
||||
//this creates an app
|
||||
createExpressServer(client);
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Creating the Example Job
|
||||
|
||||
Create a Job file. In this case created `<root>/jobs/example.(ts/js)`
|
||||
|
||||
```typescript jobs/example.(ts/js)
|
||||
import { eventTrigger } from "@trigger.dev/sdk";
|
||||
import { client } from "../trigger";
|
||||
|
||||
// your first job
|
||||
client.defineJob({
|
||||
id: "example-job",
|
||||
name: "Example Job",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "example.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("Hello world!", { payload });
|
||||
|
||||
return {
|
||||
message: "Hello world!",
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Adding Configuration to `package.json`
|
||||
|
||||
Inside the `package.json` file, add the following configuration under the root object:
|
||||
|
||||
```json
|
||||
"trigger.dev": {
|
||||
"endpointId": "my-app"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Replace **"my-app"** with the appropriate identifier you used in the trigger.js configuration file.
|
||||
|
||||
## Running
|
||||
|
||||
### Run your Express app
|
||||
|
||||
Run your Express app locally, like you normally would. For example:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Note>You might use `npm run start` instead of dev</Note>
|
||||
|
||||
### Run the CLI 'dev' command
|
||||
|
||||
In a **_separate terminal window or tab_** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
<br />
|
||||
<Note>
|
||||
You can optionally pass the port if you're not running on 3000 by adding
|
||||
`--port 3001` to the end
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
You can optionally pass the hostname if you're not running on localhost by adding
|
||||
`--hostname <host>`. Example, in case your Express is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
</Note>
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
<Accordion defaultOpen title="Don't have a NestJS project yet to add Trigger.dev to? No problem, you can complete the Manual Setup using a blank NestJS project:">
|
||||
Create a blank project by installing the NestJS CLI in your terminal:
|
||||
|
||||
```bash
|
||||
npm i -g @nestjs/cli
|
||||
```
|
||||
|
||||
Then, create an empty project with:
|
||||
|
||||
```bash
|
||||
nest new project-name
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Installing Required Packages
|
||||
|
||||
To begin, install the necessary packages in your NestJS project directory. You can choose one of the following package managers:
|
||||
|
||||
<CodeGroup>
|
||||
```bash npm
|
||||
npm i @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk @trigger.dev/nestjs @nestjs/config
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<Note>Ensure that you execute this command within a NestJS project.</Note>
|
||||
|
||||
## Obtaining the Development API Key
|
||||
|
||||
To locate your development API key, login to the [Trigger.dev
|
||||
dashboard](https://cloud.trigger.dev) and select the Project you want to
|
||||
connect to. Then click on the Environments & API Keys tab in the left menu.
|
||||
You can copy your development API Key from the field at the top of this page.
|
||||
(Your development key will start with `tr_dev_`).
|
||||
|
||||
## Adding Environment Variables
|
||||
|
||||
Create a `.env` file at the root of your project and include your Trigger API key and URL like this:
|
||||
|
||||
```bash
|
||||
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL=https://api.trigger.dev # this line is only necessary if you are self-hosting Trigger
|
||||
```
|
||||
|
||||
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
|
||||
|
||||
<Note>
|
||||
This configuration only will be loaded if you use [NestJS
|
||||
Config](https://docs.nestjs.com/techniques/configuration) or
|
||||
[dotenv](https://github.com/motdotla/dotenv).
|
||||
</Note>
|
||||
|
||||
## Adding TriggerDev Module
|
||||
|
||||
Open your `app.module.ts`, and add the following inside your `imports`:
|
||||
|
||||
```typescript
|
||||
import { TriggerDevModule } from "@trigger.dev/nestjs";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
//you need to load the environment variables from .env, this is one way to do it
|
||||
import "dotenv/config";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TriggerDevModule.register({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
}),
|
||||
// if you use NestJS Config, you can do like this:
|
||||
// TriggerDevModule.registerAsync({
|
||||
// useFactory: (configService: ConfigService) => ({
|
||||
// id: 'my-app',
|
||||
// apiKey: configService.get<string>("TRIGGER_API_KEY"),
|
||||
// apiUrl: configService.get<string>("TRIGGER_API_URL"),
|
||||
// }),
|
||||
// inject: [ConfigService],
|
||||
// }),
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier.
|
||||
|
||||
By following these steps, you'll configure the Trigger Client to work with your project.
|
||||
|
||||
## Creating the Example Job
|
||||
|
||||
When you add `TriggerDevModule` to your project, you will can have access to the `TriggerClient` instance by using the `@InjectTriggerDevClient()` decorator in the constructor.
|
||||
|
||||
Now, let's create an example job to test the integration.
|
||||
|
||||
1. Create a controller named `job.controller.ts` alongside your `app.module.ts`
|
||||
2. Inside that controller, add the following code:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```typescript job.controller.ts
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { InjectTriggerDevClient } from "@trigger.dev/nestjs";
|
||||
import { eventTrigger, TriggerClient } from "@trigger.dev/sdk";
|
||||
|
||||
@Controller()
|
||||
export class JobController {
|
||||
constructor(@InjectTriggerDevClient() private readonly client: TriggerClient) {
|
||||
this.client.defineJob({
|
||||
id: "test-job",
|
||||
name: "Test Job One",
|
||||
version: "0.0.1",
|
||||
trigger: eventTrigger({
|
||||
name: "test.event",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info("Hello world!", { payload });
|
||||
|
||||
return {
|
||||
message: "Hello world!",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return `Running Trigger.dev with client-id ${this.client.id}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, add this controller to your `app.module.ts`:
|
||||
|
||||
```typescript app.module.ts
|
||||
import { TriggerDevModule } from "@trigger.dev/nestjs";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JobController } from "./job.controller";
|
||||
|
||||
//you need to load the environment variables from .env, this is one way to do it
|
||||
import "dotenv/config";
|
||||
|
||||
@Module({
|
||||
controllers: [JobController],
|
||||
imports: [
|
||||
TriggerDevModule.register({
|
||||
id: "my-app",
|
||||
apiKey: process.env.TRIGGER_API_KEY,
|
||||
apiUrl: process.env.TRIGGER_API_URL,
|
||||
}),
|
||||
// if you use NestJS Config, you can do like this:
|
||||
// TriggerDevModule.registerAsync({
|
||||
// useFactory: (configService: ConfigService) => ({
|
||||
// id: 'my-app',
|
||||
// apiKey: configService.get<string>("TRIGGER_API_KEY"),
|
||||
// apiUrl: configService.get<string>("TRIGGER_API_URL"),
|
||||
// }),
|
||||
// inject: [ConfigService],
|
||||
// }),
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<br />
|
||||
<Note>
|
||||
You can import the Trigger.dev client inside any `service` or `controller`, we recommend you to
|
||||
create specialized `service` for each job you have for a better maintainability.
|
||||
</Note>
|
||||
|
||||
## Adding Configuration to `package.json`
|
||||
|
||||
Inside the `package.json` file, add the following configuration under the root object:
|
||||
|
||||
```json
|
||||
"trigger.dev": {
|
||||
"endpointId": "my-app"
|
||||
}
|
||||
```
|
||||
|
||||
Your `package.json` file might look something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-app",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
// ... other dependencies
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "my-app"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client.
|
||||
|
||||
## Running
|
||||
|
||||
### Run your NestJS app
|
||||
|
||||
Run your NestJS app locally, like you normally would. For example:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm run start
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn run start
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Run the CLI 'dev' command
|
||||
|
||||
In a **_separate terminal window or tab_** run:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn dlx @trigger.dev/cli@latest dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
<br />
|
||||
<Note>
|
||||
You can optionally pass the port if you're not running on 3000 by adding
|
||||
`--port 3001` to the end
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
You can optionally pass the hostname if you're not running on localhost by adding
|
||||
`--hostname <host>`. Example, in case your Remix is running on 0.0.0.0: `--hostname 0.0.0.0`.
|
||||
</Note>
|
||||
@@ -28,6 +28,7 @@ Each platform has one or more adaptors, see the guides below:
|
||||
| ------------------------------------------------- | -------------------- |
|
||||
| [Next.js](/documentation/guides/platforms/nextjs) | `createPagesRoute()` |
|
||||
| [Next.js](/documentation/guides/platforms/nextjs) | `createAppRoute()` |
|
||||
| [NestJS](/documentation/guides/manual/nestjs) | `TriggerDevModule` |
|
||||
| [Astro](/documentation/guides/platforms/astro) | `createAstroRoute()` |
|
||||
| [Remix](/documentation/guides/platforms/remix) | `createRemixRoute()` |
|
||||
| Express | Coming soon |
|
||||
|
||||
@@ -27,6 +27,10 @@ The `DEV` environment should only be used for local development. It's where you
|
||||
|
||||
<Snippet file="scheduled-dev-warning.mdx" />
|
||||
|
||||
### Staging
|
||||
|
||||
The `STAGING` environment is useful for testing your Jobs against your staging server, if you have one. STAGING works identically to PROD.
|
||||
|
||||
### Production
|
||||
|
||||
The `PROD` environment is where your Jobs will run in production. It's where you can run your Jobs against real data.
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
---
|
||||
title: Introduction
|
||||
title: "Triggers: Introduction"
|
||||
sidebarTitle: "Introduction"
|
||||
description: "A Trigger is what starts a Job Run. It can be a webhook, a schedule, or an event."
|
||||
---
|
||||
|
||||
We currently support three types of Triggers: Webhooks, Scheduled, and Events. You can use any of these to start a Job Run.
|
||||
|
||||
<CardGroup>
|
||||
<Card
|
||||
title="Webhooks"
|
||||
icon="webhook"
|
||||
href="/documentation/concepts/triggers/webhooks"
|
||||
>
|
||||
<Card title="Webhooks" icon="webhook" href="/documentation/concepts/triggers/webhooks">
|
||||
Start your Jobs in realtime when events happen in APIs
|
||||
</Card>
|
||||
<Card
|
||||
title="Scheduled"
|
||||
icon="calendar"
|
||||
href="/documentation/concepts/triggers/scheduled"
|
||||
>
|
||||
<Card title="Scheduled" icon="calendar" href="/documentation/concepts/triggers/scheduled">
|
||||
Run a Job on a repeating schedule
|
||||
</Card>
|
||||
<Card
|
||||
title="Event"
|
||||
icon="brackets-curly"
|
||||
href="/documentation/concepts/triggers/events"
|
||||
>
|
||||
<Card title="Event" icon="brackets-curly" href="/documentation/concepts/triggers/events">
|
||||
Run your Job when you send events with data
|
||||
</Card>
|
||||
<Card
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "Introduction"
|
||||
title: "Deployment: Introduction"
|
||||
sidebarTitle: "Introduction"
|
||||
description: "A guide for how to deploy your Jobs"
|
||||
---
|
||||
|
||||
@@ -10,11 +11,7 @@ Deployment uses [Environments & Endpoints](/documentation/concepts/environments-
|
||||
The first time you deploy to a new environment you will need to setup the
|
||||
endpoint for that environment.
|
||||
|
||||
<Card
|
||||
title="First time setup"
|
||||
icon="wrench"
|
||||
href="/documentation/guides/deployment-setup"
|
||||
>
|
||||
<Card title="First time setup" icon="wrench" href="/documentation/guides/deployment-setup">
|
||||
This only needs to be done once for each environment
|
||||
</Card>
|
||||
|
||||
@@ -32,11 +29,7 @@ There are two ways to do this:
|
||||
>
|
||||
Manually refresh in your Trigger.dev dashboard
|
||||
</Card>
|
||||
<Card
|
||||
title="Automatic refreshing"
|
||||
icon="robot"
|
||||
href="/documentation/guides/deployment-automatic"
|
||||
>
|
||||
<Card title="Automatic refreshing" icon="robot" href="/documentation/guides/deployment-automatic">
|
||||
Automatically refresh by using our webhook
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "NestJS"
|
||||
sidebarTitle: "NestJS"
|
||||
description: "How to manually setup Trigger.dev in your NestJS project"
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-nestjs.mdx" />
|
||||
@@ -22,15 +22,15 @@ To begin, install the necessary packages in your Next.js project directory. You
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i @trigger.dev/sdk @trigger-dev/nextjs
|
||||
npm i @trigger.dev/sdk @trigger.dev/nextjs
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install @trigger.dev/sdk @trigger-dev/nextjs
|
||||
pnpm install @trigger.dev/sdk @trigger.dev/nextjs
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/sdk @trigger-dev/nextjs
|
||||
yarn add @trigger.dev/sdk @trigger.dev/nextjs
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "Overview"
|
||||
title: "React hooks: Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "How to show the live status of Job Runs in your React app"
|
||||
---
|
||||
|
||||
@@ -48,38 +49,110 @@ This guide assumes that your project is already setup and you have a Job running
|
||||
</Accordion>
|
||||
|
||||
</Step>
|
||||
<Step title="Add the env var to your project">
|
||||
Add the `NEXT_PUBLIC_TRIGGER_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
<Step title="Setting up environment variables">
|
||||
<Tabs>
|
||||
<Tab title="Next.js">
|
||||
Add the `NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
|
||||
```sh .env.local
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
NEXT_PUBLIC_TRIGGER_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
```sh .env.local
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
|
||||
Your private API key should already be in there.
|
||||
Your private API key should already be in there.
|
||||
|
||||
`NEXT_PUBLIC_` is a special prefix that exposes the environment variable to your users' web browsers.
|
||||
</Tab>
|
||||
|
||||
<Tab title="Remix/React">
|
||||
Add the `TRIGGER_PUBLIC_API_KEY` environment variable to your project. This will be used by the `TriggerProvider` component to connect to the Trigger API.
|
||||
|
||||
```sh .env
|
||||
#...
|
||||
TRIGGER_API_KEY=[your_private_api_key]
|
||||
TRIGGER_PUBLIC_API_KEY=[your_public_api_key]
|
||||
#...
|
||||
```
|
||||
|
||||
You will need to pass this value from the server to the client. We recommend you do this in your Root loader.
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add the <TriggerProvider> component">
|
||||
|
||||
The [TriggerProvider](/sdk/react/triggerprovider) component is a React Context Provider that will make the Trigger API client available to all child components.
|
||||
|
||||
Generally you'll want to add this to the root of your app, so that it's available everywhere. However, you can add it lower in the hierarchy but it must be above any of the hooks.
|
||||
|
||||
```tsx app/layout.tsx
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_API_KEY!}>
|
||||
{children}
|
||||
</TriggerProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Next.js">
|
||||
|
||||
```tsx app/layout.tsx
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<TriggerProvider publicApiKey={process.env.NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY!}>
|
||||
{children}
|
||||
</TriggerProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Remix">
|
||||
|
||||
```tsx app/root.tsx
|
||||
//return the public key env var from the loader so it's available in the browser
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
//...other code
|
||||
|
||||
const triggerPublicApiKey = env.TRIGGER_PUBLIC_API_KEY!;
|
||||
|
||||
return json({
|
||||
//...other data
|
||||
triggerPublicApiKey
|
||||
});
|
||||
}
|
||||
|
||||
//Your default export, i.e. the page component
|
||||
export default function App() {
|
||||
const {
|
||||
//...other data
|
||||
triggerPublicApiKey
|
||||
} = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
{/* wrap your outlet in this */}
|
||||
<TriggerProvider publicApiKey={triggerPublicApiKey}>
|
||||
<Outlet />
|
||||
</TriggerProvider>
|
||||
<ScrollRestoration />
|
||||
<ExternalScripts />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -14,9 +14,32 @@ There's a tab on the Job page called **Test**. Or you can click the "Test" butto
|
||||
|
||||

|
||||
|
||||
1. Select the environment you'd like the test to run against.
|
||||
2. Some Triggers provide example payloads that you can select from. This will populate the code editor below.
|
||||
3. When you're happy with the payload, click **Run test**.
|
||||
<Steps>
|
||||
<Step title="Edit JSON payload">
|
||||
You will see errors inline if you have any syntax errors. You can use the *Clear* and *Copy*
|
||||
buttons in the corner.
|
||||
</Step>
|
||||
<Step title="Example payloads">
|
||||
Some Triggers provide example payloads that you can select from. When selected they will
|
||||
populate the code editor below.
|
||||
</Step>
|
||||
<Step title="Recent payloads">
|
||||
If you have previously done Runs, you can select from the most recent payloads. When selected
|
||||
they will populate the code editor below.
|
||||
</Step>
|
||||
<Step title="Account ID">
|
||||
If this Job has associated Accounts, enter an Account ID. See [testing with account
|
||||
ids](/documentation/guides/using-integrations-byo-auth#testing-jobs-with-account-id) for more
|
||||
information.
|
||||
</Step>
|
||||
<Step title="Environment selection">
|
||||
Select the environment you'd like the test to run against.
|
||||
</Step>
|
||||
<Step title="Run test">
|
||||
When you're happy with the payload, click **Run test**. Or press the shortcut key: `Cmd + Enter`
|
||||
on Mac, `Ctrl + Enter` on Windows.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Identifying test runs
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Integrations Overview"
|
||||
description: "How to use Trigger.dev Integrations"
|
||||
title: "Integrations: Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "How to use Trigger.dev Integrations"
|
||||
---
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Introduction
|
||||
title: "Getting Started: Introduction"
|
||||
sidebarTitle: "Introduction"
|
||||
description: "Welcome to the Trigger.dev documentation."
|
||||
---
|
||||
|
||||
|
||||
@@ -4,4 +4,81 @@ sidebarTitle: "Astro"
|
||||
description: "Start creating Jobs in 5 minutes in your Astro project."
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-astro.mdx" />
|
||||
This quick start guide will get you up and running with Trigger.dev.
|
||||
|
||||
<Accordion title="Need to create a new Astro project to add Trigger.dev to?">
|
||||
No problem, create a blank project by running the `create-astro` command in your terminal then continue with this quickstart guide as normal:
|
||||
|
||||
```bash
|
||||
npx create-astro@latest
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Steps titleSize="h3">
|
||||
<Snippet file="quickstart-setup-steps.mdx" />
|
||||
|
||||
<Step title="Run the CLI `dev` command">
|
||||
|
||||
<Snippet file="quickstart-cli-dev.mdx" />
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Advanced: Run your Astro server together with the CLI">
|
||||
You can modify your `package.json` to run both the Astro server and the CLI `dev` command together.
|
||||
|
||||
1. Install the `concurrently` package:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm install concurrently --save-dev
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add concurrently --dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
2. Modify your `package.json` file's `dev` script.
|
||||
|
||||
```json package.json
|
||||
//...
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others npm:dev:*",
|
||||
//your normal astro dev command would go here
|
||||
"dev:astro": "astro dev",
|
||||
"dev:trigger": "npx @trigger.dev/cli dev",
|
||||
//...
|
||||
}
|
||||
//...
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Your first job">
|
||||
|
||||
The CLI init command created a simple Job for you. There will be a new file `src/jobs/example.(ts/js)`.
|
||||
|
||||
In there is this Job:
|
||||
|
||||
<Snippet file="quickstart-example-job.mdx" />
|
||||
|
||||
If you navigate to your Trigger.dev project you will see this Job in the "Jobs" section:
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Snippet file="quickstart-running-your-job.mdx" />
|
||||
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "NestJS Quick Start"
|
||||
sidebarTitle: "NestJS"
|
||||
description: "Start creating Jobs in 5 minutes in your NestJS project."
|
||||
---
|
||||
|
||||
<Snippet file="manual-setup-nestjs.mdx" />
|
||||
@@ -87,3 +87,6 @@ If you navigate to your Trigger.dev project you will see this Job in the "Jobs"
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
<CardGroup cols={2}>
|
||||
<Snippet file="card-react-hooks.mdx" />
|
||||
</CardGroup>
|
||||
|
||||
@@ -67,7 +67,7 @@ yarn add concurrently --dev
|
||||
|
||||
<Step title="Your first job">
|
||||
|
||||
The CLI init command created a simple Job for you. There will be a new file either `app/jobs/example.server.(ts/js)`.
|
||||
The CLI init command created a simple Job for you. There will be a new file `app/jobs/example.server.(ts/js)`.
|
||||
|
||||
In there is this Job:
|
||||
|
||||
@@ -84,3 +84,6 @@ If you navigate to your Trigger.dev project you will see this Job in the "Jobs"
|
||||
</Steps>
|
||||
|
||||
<Snippet file="quickstart-whats-next.mdx" />
|
||||
<CardGroup cols={2}>
|
||||
<Snippet file="card-react-hooks.mdx" />
|
||||
</CardGroup>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 290 KiB |
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Introduction
|
||||
title: "GitHub: Introduction"
|
||||
sidebarTitle: "Introduction"
|
||||
---
|
||||
|
||||
<Snippet file="integration-getting-started.mdx" />
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: Replicate
|
||||
description: "Run machine learning tasks easily at scale"
|
||||
---
|
||||
|
||||
<Snippet file="integration-getting-started.mdx" />
|
||||
|
||||
## Installation
|
||||
|
||||
To get started with the Replicate integration on Trigger.dev, you need to install the `@trigger.dev/replicate` package.
|
||||
You can do this using npm, pnpm, or yarn:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install @trigger.dev/replicate@latest
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add @trigger.dev/replicate@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/replicate@latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Authentication
|
||||
|
||||
To use the Replicate API with Trigger.dev, you have to provide an API Key.
|
||||
|
||||
### API Key
|
||||
|
||||
You can create an API Key in your [Account Settings](https://replicate.com/account/api-tokens).
|
||||
|
||||
```ts
|
||||
import { Replicate } from "@trigger.dev/replicate";
|
||||
|
||||
//this will use the passed in API key (defined in your environment variables)
|
||||
const replicate = new Replicate({
|
||||
id: "replicate",
|
||||
apiKey: process.env["REPLICATE_API_KEY"],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Include the Replicate integration in your Trigger.dev job.
|
||||
|
||||
```ts
|
||||
client.defineJob({
|
||||
id: "replicate-cinematic-prompt",
|
||||
name: "Replicate - Cinematic Prompt",
|
||||
version: "0.1.0",
|
||||
integrations: { replicate },
|
||||
trigger: eventTrigger({
|
||||
name: "replicate.cinematic",
|
||||
schema: z.object({
|
||||
prompt: z.string().default("rick astley riding a harley through post-apocalyptic miami"),
|
||||
version: z
|
||||
.string()
|
||||
.default("af1a68a271597604546c09c64aabcd7782c114a63539a4a8d14d1eeda5630c33"),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//wait for prediction completion (uses remote callbacks internally)
|
||||
const prediction = await io.replicate.predictions.createAndAwait("await-prediction", {
|
||||
version: payload.version,
|
||||
input: {
|
||||
prompt: `${payload.prompt}, cinematic, 70mm, anamorphic, bokeh`,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
});
|
||||
return prediction.output;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
You can paginate responses:
|
||||
|
||||
- Using the `getAll` helper
|
||||
- Using the `paginate` helper
|
||||
|
||||
```ts
|
||||
client.defineJob({
|
||||
id: "replicate-pagination",
|
||||
name: "Replicate Pagination",
|
||||
version: "0.1.0",
|
||||
integrations: {
|
||||
replicate,
|
||||
},
|
||||
trigger: eventTrigger({
|
||||
name: "replicate.paginate",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
// getAll - returns an array of all results (uses paginate internally)
|
||||
const all = await io.replicate.getAll(io.replicate.predictions.list, "get-all");
|
||||
|
||||
// paginate - returns an async generator, useful to process one page at a time
|
||||
for await (const predictions of io.replicate.paginate(
|
||||
io.replicate.predictions.list,
|
||||
"paginate-all"
|
||||
)) {
|
||||
await io.logger.info("stats", {
|
||||
total: predictions.length,
|
||||
versions: predictions.map((p) => p.version),
|
||||
});
|
||||
}
|
||||
|
||||
return { count: all.length };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Tasks
|
||||
|
||||
### Collections
|
||||
|
||||
| Function Name | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------- |
|
||||
| `collections.get` | Gets a collection. |
|
||||
| `collections.list` | Returns the first page of all collections. Use with pagination helper. |
|
||||
|
||||
### Deployments
|
||||
|
||||
| Function Name | Description |
|
||||
| ---------------------------------------- | --------------------------------------------------------- |
|
||||
| `deployments.predictions.create` | Creates a new prediction with a deployment. |
|
||||
| `deployments.predictions.createAndAwait` | Creates and waits for a new prediction with a deployment. |
|
||||
|
||||
### Models
|
||||
|
||||
| Function Name | Description |
|
||||
| ----------------- | ------------------------ |
|
||||
| `models.get` | Gets a model. |
|
||||
| `models.versions` | Gets a model version. |
|
||||
| `models.versions` | Gets all model versions. |
|
||||
|
||||
### Predictions
|
||||
|
||||
| Function Name | Description |
|
||||
| ---------------------------- | ---------------------------------------------------------------------- |
|
||||
| `predictions.cancel` | Cancels a prediction. |
|
||||
| `predictions.create` | Creates a prediction. |
|
||||
| `predictions.createAndAwait` | Creates and waits for a prediction. |
|
||||
| `predictions.get` | Gets a prediction. |
|
||||
| `predictions.list` | Returns the first page of all predictions. Use with pagination helper. |
|
||||
|
||||
### Trainings
|
||||
|
||||
| Function Name | Description |
|
||||
| -------------------------- | -------------------------------------------------------------------- |
|
||||
| `trainings.cancel` | Cancels a training. |
|
||||
| `trainings.create` | Creates a training. |
|
||||
| `trainings.createAndAwait` | Creates and waits for a training. |
|
||||
| `trainings.get` | Gets a training. |
|
||||
| `trainings.list` | Returns the first page of all trainings. Use with pagination helper. |
|
||||
|
||||
### Misc
|
||||
|
||||
| Function Name | Description |
|
||||
| ------------- | --------------------------------------------------- |
|
||||
| `getAll` | Pagination helper that returns an array of results. |
|
||||
| `paginate` | Pagination helper that returns an async generator. |
|
||||
| `request` | Sends authenticated requests to the Replicate API. |
|
||||
| `run` | Creates and waits for a prediction. |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user