Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d901bc881 |
@@ -1,11 +1,6 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
|
||||
"changelog": [
|
||||
"@remix-run/changelog-github",
|
||||
{
|
||||
"repo": "triggerdotdev/trigger.dev"
|
||||
}
|
||||
],
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [
|
||||
[
|
||||
|
||||
+13
-4
@@ -1,35 +1,43 @@
|
||||
*.log
|
||||
\*.log
|
||||
.git
|
||||
.github
|
||||
|
||||
# editor
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# dependencies
|
||||
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
|
||||
coverage
|
||||
|
||||
# next.js
|
||||
|
||||
.next/
|
||||
build
|
||||
|
||||
# packages
|
||||
|
||||
build
|
||||
dist
|
||||
packages/**/dist
|
||||
packages/\*\*/dist
|
||||
|
||||
# misc
|
||||
|
||||
.DS_Store
|
||||
*.pem
|
||||
\*.pem
|
||||
|
||||
.turbo
|
||||
.vercel
|
||||
.cache
|
||||
.output
|
||||
apps/**/public/build
|
||||
apps/\*\*/public/build
|
||||
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
@@ -38,6 +46,7 @@ apps/**/styles/tailwind.css
|
||||
packages/**/styles/tailwind.css
|
||||
|
||||
.changeset
|
||||
references
|
||||
examples
|
||||
CHANGESETS.md
|
||||
CONTRIBUTING.md
|
||||
|
||||
@@ -31,6 +31,8 @@ CLOUD_AIRTABLE_CLIENT_ID=
|
||||
CLOUD_AIRTABLE_CLIENT_SECRET=
|
||||
CLOUD_GITHUB_CLIENT_ID=
|
||||
CLOUD_GITHUB_CLIENT_SECRET=
|
||||
CLOUD_LINEAR_CLIENT_ID=
|
||||
CLOUD_LINEAR_CLIENT_SECRET=
|
||||
CLOUD_SLACK_APP_HOST=
|
||||
CLOUD_SLACK_CLIENT_ID=
|
||||
CLOUD_SLACK_CLIENT_SECRET=
|
||||
@@ -0,0 +1,38 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Create a bug report to help us improve
|
||||
title: "bug: "
|
||||
labels: ["🐞 unconfirmed bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Provide environment information
|
||||
description: |
|
||||
Run this command in your project root and paste the results:
|
||||
```bash
|
||||
npx envinfo --system --binaries
|
||||
```
|
||||
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Reproduction repo
|
||||
description: If applicable, please provide a link to a reproduction repo or a Stackblitz / CodeSandbox project. Your issue may be closed if this is not provided and we are unable to reproduce the issue. If your bug is a docs issue, link the appropriate page.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask a Question
|
||||
url: https://trigger.dev/discord
|
||||
about: Ask questions and discuss with other community members
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "feat: "
|
||||
labels: ["🌟 enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternate solutions
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
|
||||
@@ -0,0 +1,12 @@
|
||||
"📌 area: cli":
|
||||
- any: ["cli/**/*"]
|
||||
|
||||
"📌 area: t3-app":
|
||||
- any: ["cli/template/**/*"]
|
||||
|
||||
"📚 documentation":
|
||||
- any: ["www/**/*"]
|
||||
- any: ["**/*.md"]
|
||||
|
||||
"📌 area: ci":
|
||||
- any: [".github/**/*"]
|
||||
@@ -0,0 +1,27 @@
|
||||
Closes #<issue>
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md)
|
||||
- [ ] The PR title follows the convention.
|
||||
- [ ] I ran and tested the code works
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
_[Describe the steps you took to test this change]_
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
_[Short description of what has changed]_
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
_[Screenshots]_
|
||||
|
||||
💯
|
||||
@@ -129,10 +129,10 @@ jobs:
|
||||
run: |
|
||||
# Setup environment variables
|
||||
cp ./.env.example ./.env
|
||||
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
|
||||
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @examples/nextjs-test^...
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
|
||||
Vendored
+9
@@ -19,6 +19,15 @@
|
||||
"name": "Chrome webapp",
|
||||
"url": "http://localhost:3030",
|
||||
"webRoot": "${workspaceFolder}/apps/webapp/app"
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug BYO Auth",
|
||||
"command": "pnpm run byo-auth",
|
||||
"envFile": "${workspaceFolder}/references/job-catalog/.env",
|
||||
"cwd": "${workspaceFolder}/references/job-catalog",
|
||||
"sourceMaps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,3 +27,12 @@ Please follow the best-practice of adding changesets in the same commit as the c
|
||||
3. Create version `pnpm run changeset:version`
|
||||
4. Release `pnpm run changeset:release`
|
||||
5. Switch back to normal mode by running `pnpm run changeset:normal`
|
||||
|
||||
## Snapshot instructions
|
||||
|
||||
!MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW!
|
||||
|
||||
1. Add changesets as usual `pnpm run changeset:add`
|
||||
2. Create a snapshot version (replace "dev" with your tag) `pnpm exec changeset version --snapshot dev`
|
||||
3. Build the packages: `pnpm run build --filter "@trigger.dev/*"`
|
||||
4. Publish the snapshot (replace "dev" with your tag) `pnpm exec changeset publish --no-git-tag --snapshot --tag dev`
|
||||
|
||||
+21
-13
@@ -23,11 +23,11 @@ branch are tagged into a release monthly.
|
||||
1. Clone the repo into a public GitHub repository or [fork the repo](https://github.com/triggerdotdev/trigger.dev/fork). If you plan to distribute the code, keep the source code public to comply with the [Apache Licence 2.0](https://github.com/triggerdotdev/trigger.dev/blob/main/LICENSE).
|
||||
|
||||
```
|
||||
git clone https://github.com/triggerdotdev/trigger.dev.git
|
||||
git clone https://github.com/<github_username>/trigger.dev.git
|
||||
```
|
||||
|
||||
> If you are on windows, run the following command on gitbash with admin privileges:
|
||||
> `git clone -c core.symlinks=true https://github.com/triggerdotdev/trigger.dev.git`
|
||||
> `git clone -c core.symlinks=true https://github.com/<github_username>/trigger.dev.git`
|
||||
|
||||
2. Navigate to the project folder
|
||||
```
|
||||
@@ -133,10 +133,10 @@ pnpm run dev
|
||||
|
||||
2. Open a new Terminal window and run the webapp locally and then create a new project in the dashboard. Copy out the dev API key.
|
||||
|
||||
3. Create a new temporary Next.js app in examples directory
|
||||
3. Create a new temporary Next.js app in references directory
|
||||
|
||||
```sh
|
||||
cd ./examples
|
||||
cd ./references
|
||||
pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir --import-alias "@/*"
|
||||
```
|
||||
|
||||
@@ -149,7 +149,7 @@ pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir
|
||||
}
|
||||
```
|
||||
|
||||
5. Back in the terminal, navigate into the example, and initialize the CLI. When prompted, select `self-hosted` and enter `localhost:3030` if you are testing against the local instance of Trigger.dev, or you can just use the Trigger.dev cloud. When asked for an API key, use the key you copied earlier.
|
||||
5. Back in the terminal, navigate into the reference, and initialize the CLI. When prompted, select `self-hosted` and enter `localhost:3030` if you are testing against the local instance of Trigger.dev, or you can just use the Trigger.dev cloud. When asked for an API key, use the key you copied earlier.
|
||||
|
||||
```sh
|
||||
cd ./test-cli
|
||||
@@ -179,14 +179,14 @@ To run the end-to-end tests, follow the steps below:
|
||||
|
||||
```sh
|
||||
cp ./.env.example ./.env
|
||||
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
|
||||
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
```
|
||||
|
||||
2. Set up dependencies
|
||||
|
||||
```sh
|
||||
# Build packages
|
||||
pnpm run build --filter @examples/nextjs-test^...
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
@@ -221,11 +221,11 @@ pnpm run db:studio
|
||||
|
||||
## Add sample jobs
|
||||
|
||||
The [examples/jobs-starter](./examples/jobs-starter/) project defines simple jobs you can get started with.
|
||||
The [references/job-catalog](./references/job-catalog/) project defines simple jobs you can get started with.
|
||||
|
||||
1. `cd` into `examples/jobs-starter`
|
||||
2. Create a `.env.local` file with the following content,
|
||||
replacing `[TRIGGER_DEV_API_KEY]` with an actual key:
|
||||
1. `cd` into `references/job-catalog`
|
||||
2. Create a `.env` file with the following content,
|
||||
replacing `<TRIGGER_DEV_API_KEY>` with an actual key:
|
||||
|
||||
```env
|
||||
TRIGGER_API_KEY=[TRIGGER_DEV_API_KEY]
|
||||
@@ -235,12 +235,20 @@ TRIGGER_API_URL=http://localhost:3030
|
||||
`TRIGGER_API_URL` is used to configure the URL for your Trigger.dev instance,
|
||||
where the jobs will be registered.
|
||||
|
||||
3. Run the `jobs-starter` app:
|
||||
3. Run one of the the `job-catalog` files:
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
pnpm run events
|
||||
```
|
||||
|
||||
This will open up a local server using `express` on port 8080. Then in a new terminal window you can run the trigger-cli dev command:
|
||||
|
||||
```sh
|
||||
pnpm run dev:trigger
|
||||
```
|
||||
|
||||
See the [Job Catalog](./references/job-catalog/README.md) file for more.
|
||||
|
||||
4. Navigate to your trigger.dev instance ([http://localhost:3030](http://localhost:3030/)), to see the jobs.
|
||||
You can use the test feature to trigger them.
|
||||
|
||||
|
||||
@@ -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,28 +51,28 @@ 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>
|
||||
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)}>
|
||||
<SvelteKitLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupFastifyPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupFastifyPath(organization, project)} supported>
|
||||
<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={
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Client } from "~/presenters/IntegrationsPresenter.server";
|
||||
import { UpdateOAuthForm } from "./UpdateOAuthForm";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
|
||||
export function SelectOAuthMethod({
|
||||
integration,
|
||||
@@ -76,7 +77,7 @@ export function SelectOAuthMethod({
|
||||
id="EXTERNAL"
|
||||
value="EXTERNAL"
|
||||
label="Your users"
|
||||
description="We will give you OAuth React components so you can connect as your users."
|
||||
description="Use an external authentication provider or your own user database to provide auth credentails of your users."
|
||||
variant="description"
|
||||
/>
|
||||
</RadioGroup>
|
||||
@@ -108,14 +109,19 @@ export function SelectOAuthMethod({
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Header2 className="mb-1 mt-4">User OAuth coming soon</Header2>
|
||||
<Header2 className="mb-1 mt-4">BYO Auth</Header2>
|
||||
<Paragraph spacing>
|
||||
End-user OAuth is going to be released soon. If you are interested in being an early
|
||||
beta tester then please{" "}
|
||||
<a href="mailto:founders@trigger.dev" className="text-indigo-500 underline">
|
||||
message us
|
||||
</a>
|
||||
.
|
||||
We support external authentication providers through Auth Resolvers. Read the docs to
|
||||
learn more:{" "}
|
||||
<LinkButton
|
||||
variant="secondary/small"
|
||||
LeadingIcon={"docs"}
|
||||
TrailingIcon={"external-link"}
|
||||
to="https://trigger.dev/docs/documentation/guides/using-integrations-byo-auth"
|
||||
target="_blank"
|
||||
>
|
||||
Bring your own Auth
|
||||
</LinkButton>
|
||||
</Paragraph>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -197,6 +197,8 @@ function classForJobStatus(status: JobRunStatus) {
|
||||
case "TIMED_OUT":
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
case "PENDING":
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "INVALID_PAYLOAD":
|
||||
return "text-rose-500";
|
||||
default:
|
||||
return "";
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
organizationTeamPath,
|
||||
projectEnvironmentsPath,
|
||||
projectIntegrationsPath,
|
||||
projectSetupPath,
|
||||
projectPath,
|
||||
projectSetupPath,
|
||||
projectTriggersPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
@@ -96,7 +96,7 @@ export function ProjectSideMenu() {
|
||||
data-action="environments & api keys"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<SideMenuItem
|
||||
name="Team"
|
||||
icon="team"
|
||||
@@ -118,6 +118,14 @@ export function ProjectSideMenu() {
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="onboarding"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="list"
|
||||
to="https://trigger.dev/changelog"
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Account"
|
||||
icon={UserProfilePhoto}
|
||||
@@ -146,6 +154,7 @@ function SideMenuItem({
|
||||
isCollapsed,
|
||||
forceActive,
|
||||
hasWarning = false,
|
||||
target,
|
||||
}: {
|
||||
icon: IconNames | React.ComponentType<any>;
|
||||
name: string;
|
||||
@@ -153,6 +162,7 @@ function SideMenuItem({
|
||||
isCollapsed: boolean;
|
||||
hasWarning?: boolean;
|
||||
forceActive?: boolean;
|
||||
target?: string;
|
||||
}) {
|
||||
return (
|
||||
<SimpleTooltip
|
||||
@@ -164,13 +174,14 @@ function SideMenuItem({
|
||||
LeadingIcon={icon}
|
||||
leadingIconClassName="text-dimmed"
|
||||
to={to}
|
||||
target={target}
|
||||
className={({ isActive, isPending }) => {
|
||||
if (forceActive !== undefined) {
|
||||
isActive = forceActive;
|
||||
}
|
||||
return cn(
|
||||
"relative",
|
||||
isActive
|
||||
isActive || isPending
|
||||
? "bg-slate-800 text-bright group-hover:bg-slate-800"
|
||||
: "text-dimmed group-hover:bg-slate-850 group-hover:text-bright"
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
@@ -286,9 +286,13 @@ type NavLinkPropsType = Pick<NavLinkProps, "to" | "target"> &
|
||||
Omit<React.ComponentProps<typeof ButtonContent>, "className"> & {
|
||||
className?: (props: { isActive: boolean; isPending: boolean }) => string | undefined;
|
||||
};
|
||||
export const NavLinkButton = ({ to, className, ...props }: NavLinkPropsType) => {
|
||||
export const NavLinkButton = ({ to, className, target, ...props }: NavLinkPropsType) => {
|
||||
return (
|
||||
<NavLink to={to} className={cn("group outline-none", props.fullWidth ? "w-full" : "")}>
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
target={target}
|
||||
>
|
||||
{({ isActive, isPending }) => (
|
||||
<ButtonContent className={className && className({ isActive, isPending })} {...props} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
GlobeAltIcon,
|
||||
HandRaisedIcon,
|
||||
HeartIcon,
|
||||
HomeIcon,
|
||||
KeyIcon,
|
||||
LightBulbIcon,
|
||||
ListBulletIcon,
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
UserGroupIcon,
|
||||
UserIcon,
|
||||
UserPlusIcon,
|
||||
WindowIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
XCircleIcon,
|
||||
XMarkIcon,
|
||||
@@ -74,7 +76,9 @@ const icons = {
|
||||
"arrow-left": (className: string) => <ArrowLeftIcon className={cn("text-white", className)} />,
|
||||
background: (className: string) => <CloudIcon className={cn("text-sky-400", className)} />,
|
||||
beaker: (className: string) => <BeakerIcon className={cn("text-purple-500", className)} />,
|
||||
bell: (className: string) => <BellAlertIcon className={cn("text-amber-500", className)} />,
|
||||
billing: (className: string) => <CreditCardIcon className={cn("text-teal-500", className)} />,
|
||||
browser: (className: string) => <WindowIcon className={cn("text-dimmed", className)} />,
|
||||
calendar: (className: string) => (
|
||||
<CalendarDaysIcon className={cn("text-purple-500", className)} />
|
||||
),
|
||||
@@ -111,6 +115,7 @@ const icons = {
|
||||
<HandRaisedIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
heart: (className: string) => <HeartIcon className={cn("text-rose-500", className)} />,
|
||||
house: (className: string) => <HomeIcon className={cn("text-dimmed", className)} />,
|
||||
id: (className: string) => <FingerPrintIcon className={cn("text-rose-200", className)} />,
|
||||
inactive: (className: string) => <XCircleIcon className={cn("text-rose-500", className)} />,
|
||||
info: (className: string) => <InformationCircleIcon className={cn("text-blue-500", className)} />,
|
||||
@@ -126,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+";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
|
||||
import { MatchedRun, useRun } from "~/hooks/useRun";
|
||||
import { MatchedRun } from "~/hooks/useRun";
|
||||
import { formatDuration } from "~/utils";
|
||||
import {
|
||||
RunPanel,
|
||||
|
||||
@@ -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;
|
||||
@@ -167,7 +167,13 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps
|
||||
<RunPanelHeader icon={trigger.icon} title={trigger.title} />
|
||||
<RunPanelBody>
|
||||
<RunPanelProperties
|
||||
properties={[{ label: "Event name", text: run.event.name }, ...run.properties]}
|
||||
properties={[{ label: "Event name", text: run.event.name }]
|
||||
.concat(
|
||||
run.event.externalAccount
|
||||
? [{ label: "Account ID", text: run.event.externalAccount.identifier }]
|
||||
: []
|
||||
)
|
||||
.concat(run.properties)}
|
||||
/>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
|
||||
@@ -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,14 @@ export function TriggerDetail({
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconProperty icon="id" label="Event name" value={name} />
|
||||
<RunPanelIconProperty icon="account" label="Event ID" value={id} />
|
||||
{trigger.externalAccount && (
|
||||
<RunPanelIconProperty
|
||||
icon="account"
|
||||
label="Account ID"
|
||||
value={trigger.externalAccount.identifier}
|
||||
/>
|
||||
)}
|
||||
</RunPanelIconSection>
|
||||
<RunPanelDivider />
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
@@ -55,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>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { JobRunExecution, JobRunStatus } from "@trigger.dev/database";
|
||||
import { NoSymbolIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
ExclamationTriangleIcon,
|
||||
StopIcon,
|
||||
WrenchIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import type { JobRunStatus } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { HandRaisedIcon, NoSymbolIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export function hasFinished(status: JobRunStatus): boolean {
|
||||
return (
|
||||
@@ -17,7 +16,9 @@ export function hasFinished(status: JobRunStatus): boolean {
|
||||
status === "FAILURE" ||
|
||||
status === "ABORTED" ||
|
||||
status === "TIMED_OUT" ||
|
||||
status === "CANCELED"
|
||||
status === "CANCELED" ||
|
||||
status === "UNRESOLVED_AUTH" ||
|
||||
status === "INVALID_PAYLOAD"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +49,9 @@ export function RunStatusIcon({ status, className }: { status: JobRunStatus; cla
|
||||
return <XCircleIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "TIMED_OUT":
|
||||
return <ExclamationTriangleIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "INVALID_PAYLOAD":
|
||||
return <XCircleIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
return <WrenchIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "ABORTED":
|
||||
@@ -63,26 +67,26 @@ export type RunBasicStatus = "WAITING" | "PENDING" | "RUNNING" | "COMPLETED" | "
|
||||
|
||||
export function runBasicStatus(status: JobRunStatus): RunBasicStatus {
|
||||
switch (status) {
|
||||
case "SUCCESS":
|
||||
return "COMPLETED";
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
case "QUEUED":
|
||||
case "PREPROCESSING":
|
||||
case "PENDING":
|
||||
return "PENDING";
|
||||
case "STARTED":
|
||||
return "RUNNING";
|
||||
case "QUEUED":
|
||||
return "PENDING";
|
||||
case "FAILURE":
|
||||
return "FAILED";
|
||||
case "TIMED_OUT":
|
||||
return "FAILED";
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
return "PENDING";
|
||||
case "ABORTED":
|
||||
return "FAILED";
|
||||
case "PREPROCESSING":
|
||||
return "PENDING";
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "CANCELED":
|
||||
case "ABORTED":
|
||||
case "INVALID_PAYLOAD":
|
||||
return "FAILED";
|
||||
case "SUCCESS":
|
||||
return "COMPLETED";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +112,14 @@ export function runStatusTitle(status: JobRunStatus): string {
|
||||
return "Preprocessing";
|
||||
case "CANCELED":
|
||||
return "Canceled";
|
||||
case "UNRESOLVED_AUTH":
|
||||
return "Unresolved auth";
|
||||
case "INVALID_PAYLOAD":
|
||||
return "Invalid payload";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +134,8 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
|
||||
case "QUEUED":
|
||||
return "text-amber-300";
|
||||
case "FAILURE":
|
||||
case "UNRESOLVED_AUTH":
|
||||
case "INVALID_PAYLOAD":
|
||||
return "text-rose-500";
|
||||
case "TIMED_OUT":
|
||||
return "text-amber-300";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function resolveRunConnections(
|
||||
const result: Record<string, ConnectionAuth> = {};
|
||||
|
||||
for (const connection of connections) {
|
||||
if (connection.integration.authSource === "LOCAL") {
|
||||
if (connection.integration.authSource !== "HOSTED") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -120,8 +120,12 @@ export class IntegrationClientPresenter {
|
||||
icon: integration.definition.icon,
|
||||
},
|
||||
authMethod: {
|
||||
type: integration.authMethod?.type ?? "local",
|
||||
name: integration.authMethod?.name ?? "Local Auth",
|
||||
type:
|
||||
integration.authMethod?.type ?? integration.authSource === "RESOLVER" ? "local" : "local",
|
||||
name:
|
||||
integration.authMethod?.name ?? integration.authSource === "RESOLVER"
|
||||
? "Auth Resolver"
|
||||
: "Local Auth",
|
||||
},
|
||||
help,
|
||||
};
|
||||
|
||||
@@ -125,8 +125,9 @@ export class IntegrationsPresenter {
|
||||
name: c.definition.name,
|
||||
},
|
||||
authMethod: {
|
||||
type: c.authMethod?.type ?? "local",
|
||||
name: c.authMethod?.name ?? "Local Only",
|
||||
type: c.authMethod?.type ?? c.authSource === "RESOLVER" ? "resolver" : "local",
|
||||
name:
|
||||
c.authMethod?.name ?? c.authSource === "RESOLVER" ? "Auth Resolver" : "Local Only",
|
||||
},
|
||||
authSource: c.authSource,
|
||||
setupStatus: c.setupStatus,
|
||||
|
||||
@@ -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,11 +127,17 @@ export class RunPresenter {
|
||||
},
|
||||
event: {
|
||||
select: {
|
||||
id: true,
|
||||
eventId: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
context: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
select: {
|
||||
identifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
|
||||
@@ -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;
|
||||
@@ -39,6 +40,15 @@ export class TestJobPresenter {
|
||||
payload: true,
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
select: {
|
||||
integration: {
|
||||
select: {
|
||||
authSource: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
@@ -58,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: {
|
||||
@@ -88,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,
|
||||
@@ -95,12 +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"
|
||||
),
|
||||
})),
|
||||
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,
|
||||
})),
|
||||
hasTestRuns: job._count.runs > 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,34 @@ export class TriggerDetailsPresenter {
|
||||
select: {
|
||||
event: {
|
||||
select: {
|
||||
id: true,
|
||||
eventId: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
context: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
select: {
|
||||
identifier: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
+202
-114
@@ -1,6 +1,7 @@
|
||||
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,13 +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 { Popover, PopoverContent } from "~/components/primitives/Popover";
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -23,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";
|
||||
@@ -36,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({
|
||||
@@ -69,6 +75,7 @@ const schema = z.object({
|
||||
}),
|
||||
environmentId: z.string(),
|
||||
versionId: z.string(),
|
||||
accountId: z.string().optional(),
|
||||
});
|
||||
|
||||
//todo save the chosen environment to a cookie (for that user), use it to default the env dropdown
|
||||
@@ -84,11 +91,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
}
|
||||
|
||||
const testService = new TestJobService();
|
||||
const run = await testService.call({
|
||||
environmentId: submission.value.environmentId,
|
||||
payload: submission.value.payload,
|
||||
versionId: submission.value.versionId,
|
||||
});
|
||||
const run = await testService.call(submission.value);
|
||||
|
||||
if (!run) {
|
||||
return redirectBackWithErrorMessage(
|
||||
@@ -116,21 +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 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>) => {
|
||||
@@ -139,6 +151,7 @@ export default function Page() {
|
||||
payload: currentJson.current,
|
||||
environmentId: selectedEnvironmentId,
|
||||
versionId: selectedEnvironment?.versionId ?? "",
|
||||
...(currentAccountId ? { accountId: currentAccountId } : {}),
|
||||
},
|
||||
{
|
||||
action: "",
|
||||
@@ -147,10 +160,10 @@ export default function Page() {
|
||||
);
|
||||
e.preventDefault();
|
||||
},
|
||||
[currentJson, selectedEnvironmentId]
|
||||
[currentJson, selectedEnvironmentId, currentAccountId]
|
||||
);
|
||||
|
||||
const [form, { environmentId, payload }] = useForm({
|
||||
const [form, { environmentId, payload, accountId }] = useForm({
|
||||
id: "test-job",
|
||||
lastSubmission,
|
||||
onValidate({ formData }) {
|
||||
@@ -168,103 +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>
|
||||
<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>
|
||||
<div className="flex flex-none items-center justify-between">
|
||||
{payload.error ? (
|
||||
<FormError id={payload.errorId}>{payload.error}</FormError>
|
||||
)}
|
||||
<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 />
|
||||
<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>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
LeadingIcon="beaker"
|
||||
leadingIconClassName="text-bright"
|
||||
>
|
||||
Run test
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{selectedEnvironment?.hasAuthResolver && (
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
+170
-12
@@ -1,21 +1,179 @@
|
||||
import { FastifyLogo } from "~/assets/logos/FastifyLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { useState } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { RunDevCommand, TriggerDevStep, InitCommand } from "~/components/SetupCommands";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
ClientTabsContent,
|
||||
} from "~/components/primitives/ClientTabs";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Fastify" />,
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="NestJS" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
|
||||
invariant(devEnvironment, "devEnvironment is required");
|
||||
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="Fastify"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/450"
|
||||
githubIssueNumber={450}
|
||||
>
|
||||
<FastifyLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes for an existing Fastify project
|
||||
</Header1>
|
||||
<Callout
|
||||
variant={"info"}
|
||||
to="https://github.com/triggerdotdev/trigger.dev/issues/450"
|
||||
className="mb-8"
|
||||
>
|
||||
Trigger.dev has full support for serverless. We will be adding support for long-running
|
||||
servers soon.
|
||||
</Callout>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Install the necessary packages in your Fastify project directory"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<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 @trigger.dev/sdk @trigger-dev/fastify`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm install @trigger.dev/sdk @trigger-dev/fastify`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn add @trigger.dev/sdk @trigger-dev/fastify`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="2"
|
||||
title="Create a `.env` file at the root of your project and include your Trigger API key and URL like this:"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<CodeBlock
|
||||
showLineNumbers={false}
|
||||
className="mb-4"
|
||||
code={`TRIGGER_API_KEY = ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
|
||||
TRIGGER_API_URL = https://cloud.trigger.dev
|
||||
`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="3"
|
||||
title="In your project directory, create a configuration file named `trigger.ts` and add the following code:"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<CodeBlock
|
||||
showLineNumbers={false}
|
||||
className="mb-4"
|
||||
code={`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,
|
||||
});
|
||||
|
||||
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!',
|
||||
};
|
||||
},
|
||||
});
|
||||
`}
|
||||
/>
|
||||
<Paragraph spacing variant="small">
|
||||
Replace "my-app" with an appropriate identifier for your project.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="4"
|
||||
title="In `server.[js/ts]` create a middleware for the specific `/api/trigger` route."
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<CodeBlock
|
||||
showLineNumbers={false}
|
||||
className="mb-4"
|
||||
code={`
|
||||
|
||||
// In server.[js/ts]
|
||||
import fastify from "fastify";
|
||||
import { createMiddleware } from "@trigger.dev/fastify";
|
||||
import { client } from "./trigger";
|
||||
|
||||
const app = fastify({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
const middleware = createMiddleware(client);
|
||||
|
||||
app.addHook("preHandler", middleware);
|
||||
|
||||
app.listen({ port: 3000 }, () => {
|
||||
console.log("Listening on port 3000");
|
||||
});`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="5" title="Start your Fastify project" />
|
||||
<StepContentContainer> </StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="7" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</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
|
||||
|
||||
+16
-22
@@ -25,8 +25,9 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
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" />,
|
||||
@@ -38,9 +39,14 @@ export default function SetUpRemix() {
|
||||
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">
|
||||
<RemixLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
@@ -75,28 +81,16 @@ export default function SetUpRemix() {
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Follow the steps from the Remix manual installation guide"
|
||||
title="Run the CLI 'init' command in an existing Remix 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/remix"
|
||||
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.server.ts</InlineCode> to
|
||||
help you get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Remix app" />
|
||||
<StepContentContainer>
|
||||
|
||||
@@ -81,13 +81,19 @@ class CreateExternalConnectionService {
|
||||
environment: AuthenticatedEnvironment,
|
||||
payload: CreateExternalConnectionBody
|
||||
) {
|
||||
const externalAccount = await this.#prismaClient.externalAccount.findUniqueOrThrow({
|
||||
const externalAccount = await this.#prismaClient.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountIdentifier,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountIdentifier,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const integration = await this.#prismaClient.integration.findUniqueOrThrow({
|
||||
|
||||
@@ -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,151 @@
|
||||
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,
|
||||
StatusHistory,
|
||||
StatusHistorySchema,
|
||||
StatusUpdate,
|
||||
StatusUpdateData,
|
||||
StatusUpdateSchema,
|
||||
StatusUpdateState,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { 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 { JobRunStatusRecordSchema } from "@trigger.dev/core";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "PUT") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { runId, id } = ParamsSchema.parse(params);
|
||||
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
logger.debug("SetStatusService.call() request body", {
|
||||
body: anyBody,
|
||||
runId,
|
||||
id,
|
||||
});
|
||||
|
||||
const body = StatusUpdateSchema.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const service = new SetStatusService();
|
||||
|
||||
try {
|
||||
const statusRecord = await service.call(runId, id, body.data);
|
||||
|
||||
logger.debug("SetStatusService.call() response body", {
|
||||
runId,
|
||||
id,
|
||||
statusRecord,
|
||||
});
|
||||
|
||||
if (!statusRecord) {
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
|
||||
const status = JobRunStatusRecordSchema.parse({
|
||||
...statusRecord,
|
||||
state: statusRecord.state ?? undefined,
|
||||
history: statusRecord.history ?? undefined,
|
||||
data: statusRecord.data ?? undefined,
|
||||
});
|
||||
|
||||
return json(status);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export class SetStatusService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(runId: string, id: string, status: StatusUpdate) {
|
||||
const statusRecord = await $transaction(this.#prismaClient, async (tx) => {
|
||||
const existingStatus = await tx.jobRunStatusRecord.findUnique({
|
||||
where: {
|
||||
runId_key: {
|
||||
runId,
|
||||
key: id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const history: StatusHistory = [];
|
||||
const historyResult = StatusHistorySchema.safeParse(existingStatus?.history);
|
||||
if (historyResult.success) {
|
||||
history.push(...historyResult.data);
|
||||
}
|
||||
if (existingStatus) {
|
||||
history.push({
|
||||
label: existingStatus.label,
|
||||
state: (existingStatus.state ?? undefined) as StatusUpdateState,
|
||||
data: (existingStatus.data ?? undefined) as StatusUpdateData,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedStatus = await tx.jobRunStatusRecord.upsert({
|
||||
where: {
|
||||
runId_key: {
|
||||
runId,
|
||||
key: id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
key: id,
|
||||
runId,
|
||||
//this shouldn't ever use the id in reality, as the SDK makess it compulsory on the first call
|
||||
label: status.label ?? id,
|
||||
state: status.state,
|
||||
data: status.data as any,
|
||||
history: [],
|
||||
},
|
||||
update: {
|
||||
label: status.label,
|
||||
state: status.state,
|
||||
data: status.data as any,
|
||||
history: history as any[],
|
||||
},
|
||||
});
|
||||
|
||||
return updatedStatus;
|
||||
});
|
||||
|
||||
return statusRecord;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
});
|
||||
|
||||
const RecordsSchema = z.array(JobRunStatusRecordSchema);
|
||||
|
||||
export async function loader({ request, params }: LoaderArgs) {
|
||||
if (request.method.toUpperCase() === "OPTIONS") {
|
||||
return apiCors(request, json({}));
|
||||
}
|
||||
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request, { allowPublicKey: true });
|
||||
|
||||
if (!authenticationResult) {
|
||||
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
|
||||
}
|
||||
|
||||
const { runId } = ParamsSchema.parse(params);
|
||||
|
||||
logger.debug("Get run statuses", {
|
||||
runId,
|
||||
});
|
||||
|
||||
try {
|
||||
const run = await prisma.jobRun.findUnique({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
output: true,
|
||||
statuses: {
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return apiCors(request, json({ error: `No run found for id ${runId}` }, { status: 404 }));
|
||||
}
|
||||
|
||||
const parsedStatuses = RecordsSchema.parse(
|
||||
run.statuses.map((s) => ({
|
||||
...s,
|
||||
state: s.state ?? undefined,
|
||||
data: s.data ?? undefined,
|
||||
history: s.history ?? undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
return apiCors(
|
||||
request,
|
||||
json({
|
||||
run: {
|
||||
id: run.id,
|
||||
status: run.status,
|
||||
output: run.output,
|
||||
},
|
||||
statuses: parsedStatuses,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return apiCors(request, json({ error: error.message }, { status: 400 }));
|
||||
}
|
||||
|
||||
return apiCors(request, json({ error: "Something went wrong" }, { status: 500 }));
|
||||
}
|
||||
}
|
||||
@@ -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,9 +1,8 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { cors } from "remix-utils";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest, getApiKeyFromRequest } from "~/services/apiAuth.server";
|
||||
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
import { taskListToTree } from "~/utils/taskListToTree";
|
||||
|
||||
@@ -52,48 +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,
|
||||
},
|
||||
},
|
||||
const presenter = new ApiRunPresenter();
|
||||
const jobRun = await presenter.call({
|
||||
runId: runId,
|
||||
maxTasks: take,
|
||||
taskDetails: showTaskDetails,
|
||||
subTasks: query.subtasks,
|
||||
cursor: query.cursor,
|
||||
});
|
||||
|
||||
if (!jobRun) {
|
||||
@@ -122,6 +88,12 @@ export async function loader({ request, params }: LoaderArgs) {
|
||||
const { parentId, ...rest } = task;
|
||||
return { ...rest };
|
||||
}),
|
||||
statuses: jobRun.statuses.map((s) => ({
|
||||
...s,
|
||||
state: s.state ?? undefined,
|
||||
data: s.data ?? undefined,
|
||||
history: s.history ?? undefined,
|
||||
})),
|
||||
nextCursor: nextTask ? nextTask.id : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ import { logger } from "../logger.server";
|
||||
export class IngestSendEvent {
|
||||
#prismaClient: PrismaClientOrTransaction;
|
||||
|
||||
constructor(
|
||||
prismaClient: PrismaClientOrTransaction = prisma,
|
||||
private deliverEvents = true
|
||||
) {
|
||||
constructor(prismaClient: PrismaClientOrTransaction = prisma, private deliverEvents = true) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
@@ -37,71 +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.findUniqueOrThrow({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
const externalAccount = options?.accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: options.accountId,
|
||||
},
|
||||
})
|
||||
: 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);
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { airtable } from "./integrations/airtable";
|
||||
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";
|
||||
@@ -33,8 +35,10 @@ export class IntegrationCatalog {
|
||||
export const integrationCatalog = new IntegrationCatalog({
|
||||
airtable,
|
||||
github,
|
||||
linear,
|
||||
openai,
|
||||
plain,
|
||||
replicate,
|
||||
resend,
|
||||
slack,
|
||||
stripe,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { HelpSample, Integration } from "../types";
|
||||
|
||||
function usageSample(hasApiKey: boolean): HelpSample {
|
||||
return {
|
||||
title: "Using the client",
|
||||
code: `
|
||||
import { Linear } from "@trigger.dev/linear";
|
||||
|
||||
const linear = new Linear({
|
||||
id: "__SLUG__",${hasApiKey ? ",\n apiKey: process.env.LINEAR_API_KEY!" : ""}
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "linear-react-to-new-issue",
|
||||
name: "Linear - React To New Issue",
|
||||
version: "0.1.0",
|
||||
integrations: { linear },
|
||||
trigger: linear.onIssueCreated(),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.linear.createComment("create-comment", {
|
||||
issueId: payload.data.id,
|
||||
body: "Thank's for opening this issue!"
|
||||
});
|
||||
|
||||
await io.linear.createReaction("create-reaction", {
|
||||
issueId: payload.data.id,
|
||||
emoji: "+1"
|
||||
});
|
||||
|
||||
return { payload, ctx };
|
||||
},
|
||||
});
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
export const linear: Integration = {
|
||||
identifier: "linear",
|
||||
name: "Linear",
|
||||
packageName: "@trigger.dev/linear@latest",
|
||||
authenticationMethods: {
|
||||
oauth2: {
|
||||
name: "OAuth",
|
||||
type: "oauth2",
|
||||
client: {
|
||||
id: {
|
||||
envName: "CLOUD_LINEAR_CLIENT_ID",
|
||||
},
|
||||
secret: {
|
||||
envName: "CLOUD_LINEAR_CLIENT_SECRET",
|
||||
},
|
||||
},
|
||||
config: {
|
||||
authorization: {
|
||||
url: "https://linear.app/oauth/authorize",
|
||||
scopeSeparator: ",",
|
||||
},
|
||||
token: {
|
||||
url: "https://api.linear.app/oauth/token",
|
||||
metadata: {},
|
||||
},
|
||||
refresh: {
|
||||
url: "https://linear.app/oauth/authorize",
|
||||
},
|
||||
pkce: false,
|
||||
},
|
||||
scopes: [
|
||||
{
|
||||
name: "read",
|
||||
description: "Read access for the user's account. This scope must always be present.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
{
|
||||
name: "write",
|
||||
description:
|
||||
"Grants global write access to the user's account. Use a more targeted scope if you don't need full access.",
|
||||
defaultChecked: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "issues:create",
|
||||
description: "Grants access to create issues and attachments only.",
|
||||
annotations: [{ label: "Issues" }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "comments:create",
|
||||
description: "Grants access to create new issue comments.",
|
||||
annotations: [{ label: "Comments" }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "admin",
|
||||
description:
|
||||
"Grants full access to admin-level endpoints. Don't use this unless you really need it.",
|
||||
},
|
||||
],
|
||||
help: {
|
||||
samples: [usageSample(false)],
|
||||
},
|
||||
},
|
||||
apikey: {
|
||||
type: "apikey",
|
||||
help: {
|
||||
samples: [usageSample(true)],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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)],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,7 +4,14 @@ import {
|
||||
SCHEDULED_EVENT,
|
||||
TriggerMetadata,
|
||||
} from "@trigger.dev/core";
|
||||
import type { Endpoint, Integration, Job, JobIntegration, JobVersion } from "@trigger.dev/database";
|
||||
import type {
|
||||
Endpoint,
|
||||
Integration,
|
||||
Job,
|
||||
JobIntegration,
|
||||
JobIntegrationPayload,
|
||||
JobVersion,
|
||||
} from "@trigger.dev/database";
|
||||
import { DEFAULT_MAX_CONCURRENT_RUNS } from "~/consts";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
@@ -62,83 +69,7 @@ export class RegisterJobService {
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
if (jobIntegration.authSource === "LOCAL") {
|
||||
integration = await this.#prismaClient.integration.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId: environment.organizationId,
|
||||
slug: jobIntegration.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
integration = await this.#prismaClient.integration.create({
|
||||
data: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.id,
|
||||
authSource: "HOSTED",
|
||||
setupStatus: "MISSING_FIELDS",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
integration = await this.#upsertIntegrationForJobIntegration(environment, jobIntegration);
|
||||
}
|
||||
|
||||
integrations.set(jobIntegration.id, integration);
|
||||
@@ -472,6 +403,7 @@ export class RegisterJobService {
|
||||
key: job.id,
|
||||
dispatcher: eventDispatcher,
|
||||
schedule: trigger.schedule,
|
||||
organizationId: job.organizationId,
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -479,6 +411,145 @@ export class RegisterJobService {
|
||||
}
|
||||
}
|
||||
|
||||
async #upsertIntegrationForJobIntegration(
|
||||
environment: AuthenticatedEnvironment,
|
||||
jobIntegration: IntegrationConfig
|
||||
): Promise<Integration> {
|
||||
switch (jobIntegration.authSource) {
|
||||
case "LOCAL": {
|
||||
return await this.#prismaClient.integration.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId: environment.organizationId,
|
||||
slug: jobIntegration.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "LOCAL",
|
||||
connectionType: "DEVELOPER",
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
case "HOSTED": {
|
||||
return await this.#prismaClient.integration.create({
|
||||
data: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.id,
|
||||
authSource: "HOSTED",
|
||||
setupStatus: "MISSING_FIELDS",
|
||||
connectionType: "DEVELOPER",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
case "RESOLVER": {
|
||||
return await this.#prismaClient.integration.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
organizationId: environment.organizationId,
|
||||
slug: jobIntegration.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
slug: jobIntegration.id,
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "RESOLVER",
|
||||
connectionType: "EXTERNAL",
|
||||
organization: {
|
||||
connect: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
},
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
title: jobIntegration.metadata.name,
|
||||
authSource: "RESOLVER",
|
||||
connectionType: "EXTERNAL",
|
||||
definition: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: jobIntegration.metadata.id,
|
||||
},
|
||||
create: {
|
||||
id: jobIntegration.metadata.id,
|
||||
name: jobIntegration.metadata.name,
|
||||
instructions: jobIntegration.metadata.instructions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(jobIntegration.authSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #upsertJobIntegration(
|
||||
job: Job & {
|
||||
integrations: Array<JobIntegration & { integration: Integration | null }>;
|
||||
@@ -572,3 +643,7 @@ export class RegisterJobService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function assertExhaustive(x: never): never {
|
||||
throw new Error("Unexpected object: " + x);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ export class TestJobService {
|
||||
environmentId,
|
||||
versionId,
|
||||
payload,
|
||||
accountId,
|
||||
}: {
|
||||
environmentId: string;
|
||||
versionId: string;
|
||||
payload: any;
|
||||
payload?: any;
|
||||
accountId?: string;
|
||||
}) {
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
@@ -41,10 +43,27 @@ export class TestJobService {
|
||||
},
|
||||
});
|
||||
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const event = EventSpecificationSchema.parse(version.eventSpecification);
|
||||
const eventName = Array.isArray(event.name) ? event.name[0] : event.name;
|
||||
|
||||
const eventLog = await this.#prismaClient.eventRecord.create({
|
||||
const eventLog = await tx.eventRecord.create({
|
||||
data: {
|
||||
organization: {
|
||||
connect: {
|
||||
@@ -61,6 +80,13 @@ export class TestJobService {
|
||||
id: environment.id,
|
||||
},
|
||||
},
|
||||
externalAccount: externalAccount
|
||||
? {
|
||||
connect: {
|
||||
id: externalAccount.id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
eventId: `test:${eventName}:${new Date().getTime()}`,
|
||||
name: eventName,
|
||||
timestamp: new Date(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { $transaction, Prisma, PrismaClient, prisma } from "~/db.server";
|
||||
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
|
||||
|
||||
const RESUMABLE_STATUSES = ["FAILURE", "TIMED_OUT", "ABORTED", "CANCELED"];
|
||||
const RESUMABLE_STATUSES = ["FAILURE", "TIMED_OUT", "UNRESOLVED_AUTH", "ABORTED", "CANCELED"];
|
||||
|
||||
export class ContinueRunService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
CachedTaskSchema,
|
||||
RunJobError,
|
||||
RunJobInvalidPayloadError,
|
||||
RunJobResumeWithTask,
|
||||
RunJobRetryWithTask,
|
||||
RunJobSuccess,
|
||||
RunJobUnresolvedAuthError,
|
||||
RunSourceContextSchema,
|
||||
} from "@trigger.dev/core";
|
||||
import type { Task } from "@trigger.dev/database";
|
||||
@@ -261,6 +263,7 @@ export class PerformRunExecutionV1Service {
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
.map((t) => CachedTaskSchema.parse(t)),
|
||||
yieldedExecutions: run.yieldedExecutions,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
@@ -342,6 +345,21 @@ export class PerformRunExecutionV1Service {
|
||||
await this.#cancelExecution(execution);
|
||||
break;
|
||||
}
|
||||
case "UNRESOLVED_AUTH_ERROR": {
|
||||
await this.#failRunWithUnresolvedAuthError(execution, safeBody.data);
|
||||
|
||||
break;
|
||||
}
|
||||
case "INVALID_PAYLOAD": {
|
||||
await this.#failRunWithInvalidPayloadError(execution, safeBody.data);
|
||||
|
||||
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}`);
|
||||
@@ -381,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;
|
||||
|
||||
@@ -397,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,
|
||||
@@ -438,6 +492,24 @@ export class PerformRunExecutionV1Service {
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithUnresolvedAuthError(
|
||||
execution: FoundRunExecution,
|
||||
data: RunJobUnresolvedAuthError
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(tx, execution, data.issues, "UNRESOLVED_AUTH");
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithInvalidPayloadError(
|
||||
execution: FoundRunExecution,
|
||||
data: RunJobInvalidPayloadError
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(tx, execution, data.errors, "INVALID_PAYLOAD");
|
||||
});
|
||||
}
|
||||
|
||||
async #retryRunWithTask(execution: FoundRunExecution, data: RunJobRetryWithTask) {
|
||||
const { run } = execution;
|
||||
|
||||
@@ -557,7 +629,7 @@ export class PerformRunExecutionV1Service {
|
||||
prisma: PrismaClientOrTransaction,
|
||||
execution: FoundRunExecution,
|
||||
output: Record<string, any>,
|
||||
status: "FAILURE" | "ABORTED" = "FAILURE"
|
||||
status: "FAILURE" | "ABORTED" | "UNRESOLVED_AUTH" | "INVALID_PAYLOAD" = "FAILURE"
|
||||
): Promise<void> {
|
||||
const { run } = execution;
|
||||
|
||||
|
||||
@@ -1,10 +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";
|
||||
@@ -16,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";
|
||||
@@ -151,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);
|
||||
|
||||
@@ -205,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({
|
||||
@@ -244,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) {
|
||||
@@ -354,6 +391,20 @@ export class PerformRunExecutionV2Service {
|
||||
await this.#cancelExecution(run);
|
||||
break;
|
||||
}
|
||||
case "UNRESOLVED_AUTH_ERROR": {
|
||||
await this.#failRunWithUnresolvedAuthError(run, safeBody.data, durationInMs);
|
||||
|
||||
break;
|
||||
}
|
||||
case "INVALID_PAYLOAD": {
|
||||
await this.#failRunWithInvalidPayloadError(run, safeBody.data, durationInMs);
|
||||
|
||||
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}`);
|
||||
@@ -361,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 },
|
||||
@@ -394,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,
|
||||
@@ -432,6 +570,90 @@ export class PerformRunExecutionV2Service {
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithUnresolvedAuthError(
|
||||
execution: FoundRun,
|
||||
data: RunJobUnresolvedAuthError,
|
||||
durationInMs: number
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
execution,
|
||||
data.issues,
|
||||
"UNRESOLVED_AUTH",
|
||||
durationInMs
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async #failRunWithInvalidPayloadError(
|
||||
execution: FoundRun,
|
||||
data: RunJobInvalidPayloadError,
|
||||
durationInMs: number
|
||||
) {
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
await this.#failRunExecution(
|
||||
tx,
|
||||
"EXECUTE_JOB",
|
||||
execution,
|
||||
data.errors,
|
||||
"INVALID_PAYLOAD",
|
||||
durationInMs
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -556,7 +778,7 @@ export class PerformRunExecutionV2Service {
|
||||
reason: "EXECUTE_JOB" | "PREPROCESS",
|
||||
run: FoundRun,
|
||||
output: Record<string, any>,
|
||||
status: "FAILURE" | "ABORTED" | "TIMED_OUT" = "FAILURE",
|
||||
status: "FAILURE" | "ABORTED" | "TIMED_OUT" | "UNRESOLVED_AUTH" | "INVALID_PAYLOAD" = "FAILURE",
|
||||
durationInMs: number = 0
|
||||
): Promise<void> {
|
||||
await $transaction(prisma, async (tx) => {
|
||||
@@ -617,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) {
|
||||
@@ -714,6 +883,9 @@ async function findRun(prisma: PrismaClientOrTransaction, id: string) {
|
||||
output: true,
|
||||
parentId: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
version: {
|
||||
|
||||
@@ -20,6 +20,7 @@ export class ReRunService {
|
||||
version: true,
|
||||
job: true,
|
||||
event: true,
|
||||
externalAccount: true,
|
||||
},
|
||||
where: {
|
||||
id: runId,
|
||||
@@ -43,6 +44,13 @@ export class ReRunService {
|
||||
id: existingRun.environment.id,
|
||||
},
|
||||
},
|
||||
externalAccount: existingRun.externalAccount
|
||||
? {
|
||||
connect: {
|
||||
id: existingRun.externalAccount.id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
eventId: `${existingRun.event.eventId}:retry:${new Date().getTime()}`,
|
||||
name: existingRun.event.name,
|
||||
timestamp: new Date(),
|
||||
|
||||
@@ -50,11 +50,11 @@ export class StartRunService {
|
||||
integrationId: runConnection.integration.id,
|
||||
authSource: "HOSTED",
|
||||
} as const)
|
||||
: runConnection.result === "resolvedLocal"
|
||||
: runConnection.result === "resolvedLocal" || runConnection.result === "resolvedResolver"
|
||||
? ({
|
||||
key,
|
||||
integrationId: runConnection.integration.id,
|
||||
authSource: "LOCAL",
|
||||
authSource: runConnection.result === "resolvedLocal" ? "LOCAL" : "RESOLVER",
|
||||
} as const)
|
||||
: undefined
|
||||
)
|
||||
@@ -173,6 +173,7 @@ async function createRunConnections(tx: PrismaClientOrTransaction, run: FoundRun
|
||||
integration: Integration;
|
||||
}
|
||||
| { result: "resolvedLocal"; integration: Integration }
|
||||
| { result: "resolvedResolver"; integration: Integration }
|
||||
| {
|
||||
result: "missing";
|
||||
connectionType: ConnectionType;
|
||||
@@ -190,6 +191,11 @@ async function createRunConnections(tx: PrismaClientOrTransaction, run: FoundRun
|
||||
result: "resolvedLocal",
|
||||
integration: jobIntegration.integration,
|
||||
};
|
||||
} else if (jobIntegration.integration.authSource === "RESOLVER") {
|
||||
acc[jobIntegration.key] = {
|
||||
result: "resolvedResolver",
|
||||
integration: jobIntegration.integration,
|
||||
};
|
||||
} else {
|
||||
const connection = run.externalAccountId
|
||||
? await tx.integrationConnection.findFirst({
|
||||
|
||||
@@ -59,6 +59,7 @@ export class RegisterScheduleService {
|
||||
schedule: payload,
|
||||
accountId: payload.accountId,
|
||||
dynamicTrigger,
|
||||
organizationId: environment.organizationId,
|
||||
});
|
||||
|
||||
return registration;
|
||||
|
||||
@@ -16,24 +16,32 @@ export class RegisterScheduleSourceService {
|
||||
schedule,
|
||||
accountId,
|
||||
dynamicTrigger,
|
||||
organizationId,
|
||||
}: {
|
||||
key: string;
|
||||
dispatcher: EventDispatcher;
|
||||
schedule: ScheduleMetadata;
|
||||
accountId?: string;
|
||||
dynamicTrigger?: DynamicTrigger;
|
||||
organizationId: string;
|
||||
}) {
|
||||
const validatedSchedule = validateSchedule(schedule);
|
||||
|
||||
return await $transaction(this.#prismaClient, async (tx) => {
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: dispatcher.environmentId,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: dispatcher.environmentId,
|
||||
organizationId: organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -71,13 +71,19 @@ export class RegisterSourceServiceV1 {
|
||||
}
|
||||
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -71,13 +71,19 @@ export class RegisterSourceServiceV2 {
|
||||
}
|
||||
|
||||
const externalAccount = accountId
|
||||
? await tx.externalAccount.findUniqueOrThrow({
|
||||
? await tx.externalAccount.upsert({
|
||||
where: {
|
||||
environmentId_identifier: {
|
||||
environmentId: environment.id,
|
||||
identifier: accountId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
environmentId: environment.id,
|
||||
organizationId: environment.organizationId,
|
||||
identifier: accountId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -24,7 +24,6 @@ export class RegisterTriggerSourceServiceV2 {
|
||||
endpointSlug,
|
||||
id,
|
||||
key,
|
||||
accountId,
|
||||
registrationMetadata,
|
||||
}: {
|
||||
environment: AuthenticatedEnvironment;
|
||||
@@ -32,7 +31,6 @@ export class RegisterTriggerSourceServiceV2 {
|
||||
id: string;
|
||||
endpointSlug: string;
|
||||
key: string;
|
||||
accountId?: string;
|
||||
registrationMetadata?: any;
|
||||
}): Promise<RegisterSourceEventV2 | undefined> {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
@@ -63,7 +61,7 @@ export class RegisterTriggerSourceServiceV2 {
|
||||
endpoint.id,
|
||||
payload.source,
|
||||
dynamicTrigger.id,
|
||||
accountId,
|
||||
payload.accountId,
|
||||
{ id: key, metadata: registrationMetadata }
|
||||
);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user