Compare commits

...

42 Commits

Author SHA1 Message Date
James Ritchie c4da1b4a9a Updated the discord link
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-09 16:09:33 +01:00
Eric Allam 24debf0cee Slight change to the supabase job catalog 2023-08-09 16:02:31 +01:00
James Ritchie 645643ea41 Improved onboarding (#292)
* Only show the Example Jobs list when you have 1 job

* Updated the logic for displaying the different information depending on the no. of jobs you have

* Removed some unnecessary markup

* Clicking on a radio option toggles the content

* Improved the centering of the help panel

* Conetent updates to the 2 onboarding paths

* Added a prompt so you click through to run your job

* More content updates to the help panel

* Only show the prompt if the Job has never been run

* Added back in the help panel which shows the examples table if you have > 0 jobs

* Added a dialog for the I am Stuck button

* Added the new ‘new-next-app’ text for the cli command

* Refactored the ternary logic so it’s simpler

* Removed I’m stuck button and cleaned up imports

* Added in feedback button

* Added margin bottom to the Callout on the Test popover

* WIP adding a new radio button variant that takes an icon

* Added a new step to create a new next app

* The state of the tabs is stored as a URL param

* Small shadow tweak to the side panel

* Added a Discord button to the feedback menu

* Added new icons to the Named icons component

* Added a gradient to the background of the jobs page

* Made the icons take on color outside of component

* Added another NamedIcon for tree

* Fixed number

* Improved some of the typography styles

* Updated the icons

* Fixed issue with radio buttons alignment

* Added a missing step to the new next app onboarding

* Refactored example code and added all the examples to the panel

* Small icon update

* Made the title white again

* Added an icon to the storybook radiogroup

* Added a badges storybook

* Chevron table cell now takes children

* import cleanup

* Changed the background image from an arbirary value to an import

* Added a NEW badge which shows if you have a new job

* Improved the copy on the Examples help panel and updated some of the layout

* Removed defaultOpen on the help panel

* Stop the icons getting smaller

---------

Co-authored-by: D-K-P <dkp.github@pm.me>
2023-08-09 15:00:08 +01:00
Eric Allam 321378db4f Run pnpm i to fix lock file
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-09 12:40:48 +01:00
github-actions[bot] c8b10ce188 chore: Update version for release (#291)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-09 12:40:15 +01:00
Eric Allam 57ed2b2d0d Fixed cli type errors 2023-08-09 12:37:22 +01:00
Eric Allam fa3a22eb78 Added an $isNull EventFilter condition matcher 2023-08-09 12:35:47 +01:00
Dan a1bc15d18b Updated the example job (#288)
* Changed the example job to include a delay

* Improved the example job copy and added ‘in a separate terminal’ to 2.

* Create kind-eggs-pump.md

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-09 12:21:46 +01:00
Yaswanth 3f48dd0a68 [TRI-945]: Adds manual project setup guide to the docs (#286)
Co-authored-by: Crossover <crossover@MacBook-Pro.local>
2023-08-09 12:18:05 +01:00
Eric Allam 5c71868a73 Fix the examples in the supabase integration 2023-08-09 12:16:27 +01:00
Chigala 3ce6decef4 Feat/scaffold job catalog file (#283)
* feat: adds a job catalog file when an integration is created via the CLI

* added a changeset

* fix: create-integration CLI throws an error when it tries to install dependencies

* Update getUserPkgManager.ts to main

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-09 10:42:20 +01:00
Eric Allam ab007f2708 Fix the CLI typecheck error 2023-08-09 10:41:06 +01:00
Eric Allam 8412680863 Adding createImageEdit and createImageVariation tasks to openai 2023-08-09 10:39:17 +01:00
Matt Aitken a90908df6e CLI init fixes: don't ask for endpoint slug, fix for next.config and package manager artifact issue (#287)
* If an endpointId isn’t in the arguments or package, then use the project slug. Don’t ask for it.

* Should await the package artifact detection

* Catch error thrown if there’s no next.config file when detecting the type of Next project

* Create long-carrots-camp.md
2023-08-09 10:37:10 +01:00
Eric Allam a82b86210f Fix pnpm lock file for package release 2023-08-08 22:08:40 +01:00
github-actions[bot] 0367a92731 chore: Update version for release (#281)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-08 22:08:12 +01:00
Eric Allam d230cf622f Treat @trigger.dev/sdk the same as @trigger.dev/core in the webapp
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-08 15:52:14 +01:00
Eric Allam a1bf073b43 Trigger another release run 2023-08-08 15:18:13 +01:00
Eric Allam 5588108b97 Add unit test running to the public CI 2023-08-08 15:04:15 +01:00
Eric Allam b9bcecffe3 Added some Supabase storage and auth triggers to the job catalog 2023-08-08 15:02:07 +01:00
Eric Allam 59075f5fc2 EventFilter now supports more complex condition filters #271 2023-08-08 15:02:07 +01:00
Sai Hari fa942fc9f5 feat: 🎸 add end-to-end test harness (#253)
* feat: 🎸 add end-to-end test harness

* Address PR comments
2023-08-08 13:55:25 +01:00
D-K-P 47a8a557cf Replaced all the screenshots of the environments page 2023-08-08 11:47:25 +01:00
Yaswanth b51d0b5399 Fix:[TRI-879]-Hides help panel when endpoints exists on the Environme… (#256)
* Fix:[TRI-879]-Hides help panel when endpoints exists on the Environments & API Keys page

* fix: adds check for fully configured client

g

---------

Co-authored-by: Crossover <crossover@MacBook-Pro.local>
2023-08-08 11:15:58 +01:00
D-K-P 763c84e3b3 Replaced api key screenshot and text 2023-08-08 10:41:33 +01:00
Eric Allam dfb00e84e9 Fixing pnpm lock file 2023-08-08 07:35:25 +01:00
github-actions[bot] b5a64545bb chore: Update version for release (#273)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-08 07:32:24 +01:00
Eric Allam 3cf3eaff48 @trigger.dev/supabase: You can now trigger on multiple database events in the same job 2023-08-07 17:27:49 +01:00
Matt Aitken 4ca758a3de job-catalog: added initial setup and CLI build step
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-07 15:07:58 +01:00
Matt Aitken da81537009 Update job-catalog instructions
`pnpm run trigger:dev` should be `pnpm run dev:trigger`
2023-08-07 14:57:08 +01:00
Eric Allam 7c1b13ba3b Increase webhook delivery attempts to the max 25 attempts over 3 days 2023-08-07 14:25:23 +01:00
D-K-P 4b654e5b3b Fixed broken docs link 2023-08-07 13:29:11 +01:00
Eric Allam 726a50ace7 Point to the workspace version of @trigger.dev/sdk 2023-08-07 13:17:36 +01:00
github-actions[bot] 9deffb67c4 chore: Update version for release (#267)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-07 13:08:27 +01:00
Matt Aitken ff04bf44ee CLI: detect package manager using artifacts if they exist (#261)
* Detect package manager from artifacts (e.g. package-lock.json) and fallback to process.env.npm_config_user_agent

* Added a changeset
2023-08-07 13:01:22 +01:00
Matt Aitken e740297829 Cli: improve Next.js project detection (#262)
* Detect presence of a “next” dependency

* Use a strict undefined check instead

* Changeset: Detect Next.js project by looking at dependencies, not next.config.js

* Read a package json file

* First check for next.config file, otherwise use next dependency

* Update changeset description
2023-08-07 13:01:03 +01:00
D-K-P a31705e198 Added text summarizer / github issue reminder jobs to the examples table 2023-08-07 12:58:43 +01:00
Eric Allam f0bdf53364 Auto index production endpoints every 10 minutes 2023-08-07 12:49:37 +01:00
Matt Aitken 9638499163 Add your first endpoint in the dashboard (#269)
* Created the sheet

* Started work on the resource route

* Created ValidateCreateEndpointService and the EndpointValidateApi (name TBC)

* The TriggerClient responds with the id

* Tidied some stuff up

* Alternative: EndpointApi doesn’t take endpointSlug. Instead just Ping() does

* Create cool-snakes-deny.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-07 11:28:34 +01:00
Matt Aitken 9104c252b7 Telemetry (new user) (#268)
* Removed old telemetry

* Started adding TriggerClient telemetry

* Added a log if the telemetry client is created, event renamed to “user.created”
2023-08-07 11:28:13 +01:00
Matt Aitken 72cf345602 Added “thumbsRating” to docs pages 2023-08-07 10:50:02 +01:00
jemiluv8 d5b8f8299d update cli init to create index file in examples folder (#225)
* update cli init to create index file in examples folder

* add patch changeset

* Create silly-baboons-join.md

* remove comment in router file
add comment in jobs index to instruct them to

* rename examplesIndex to examplesIndexFileName

* import jobs module in routeContent for createTriggerPageRoute just as we did for createTriggerAppRoute

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-08-06 16:42:32 +01:00
146 changed files with 7117 additions and 1044 deletions
+85 -1
View File
@@ -64,8 +64,92 @@ jobs:
- name: 🔎 Type check
run: pnpm run typecheck --filter webapp
unitTests:
name: Unit Tests
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: Run Unit Tests
run: |
pnpm run test
e2e:
name: e2e Tests
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 7.18
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: |
# Setup environment variables
cp ./.env.example ./.env
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
cp ./packages/database/.env.example ./packages/database/.env
# Build packages
pnpm run build --filter @examples/nextjs-test^...
pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
pnpm install --frozen-lockfile
# Execute tests
pnpm run docker
pnpm run db:migrate
pnpm run db:seed
pnpm run test:e2e
# Cleanup
pnpm run docker:stop
- name: Upload Playwright report
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
publish:
needs: [typecheck]
needs: [typecheck, unitTests, e2e]
runs-on: buildjet-4vcpu-ubuntu-2204
outputs:
version: ${{ steps.get_version.outputs.version }}
+4 -1
View File
@@ -48,4 +48,7 @@ apps/**/public/build
.sentryclirc
.buildt
**/tmp/
**/tmp/
/test-results/
/playwright-report/
/playwright/.cache/
+46
View File
@@ -169,6 +169,52 @@ pnpm exec trigger-cli dev
9. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
## Running end-to-end webapp tests
To run the end-to-end tests, follow the steps below:
1. Set up environment variables (copy example envs into the correct place)
```sh
cp ./.env.example ./.env
cp ./examples/nextjs-test/.env.example ./examples/nextjs-test/.env.local
cp ./packages/database/.env.example ./packages/database/.env
```
2. Set up dependencies
```sh
# Build packages
pnpm run build --filter @examples/nextjs-test^...
pnpm --filter @trigger.dev/database generate
# Move trigger-cli bin to correct place
pnpm install --frozen-lockfile
```
3. Set up the database
```sh
pnpm run docker
pnpm run db:migrate
pnpm run db:seed
```
4. Run the end-to-end tests
```sh
pnpm run test:e2e
```
### Cleanup
The end-to-end tests use a `setup` and `teardown` script to seed the database with test data. If the test runner doesn't exit cleanly, then the database can be left in a state where the tests can't run because the `setup` script will try to create data that already exists. If this happens, you can manually delete the `users` and `organizations` from the database using prisma studio:
```sh
# With the database running (i.e. pnpm run docker)
pnpm run db:studio
```
## Add sample jobs
The [examples/jobs-starter](./examples/jobs-starter/) project defines simple jobs you can get started with.
@@ -0,0 +1,161 @@
export function OneTreeIcon({ className }: { className?: string }) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4914_58397)">
<g filter="url(#filter0_d_4914_58397)">
<path
d="M9 14H11C11 14 11 15 11 17C11 19 11.5 20 11.5 20H8.5C8.5 20 9 19 9 17C9 15 9 14 9 14Z"
fill="url(#paint0_linear_4914_58397)"
/>
</g>
<g filter="url(#filter1_d_4914_58397)">
<circle cx="5.5" cy="8.5" r="3.5" fill="currentColor" />
</g>
<g filter="url(#filter2_d_4914_58397)">
<circle cx="8" cy="12" r="3" fill="currentColor" />
</g>
<circle cx="13" cy="11" r="4" fill="currentColor" />
<circle cx="15.5" cy="7.5" r="2.5" fill="currentColor" />
<g filter="url(#filter3_d_4914_58397)">
<circle cx="9" cy="6" r="4" fill="currentColor" />
</g>
<circle cx="12.5" cy="5.5" r="3.5" fill="currentColor" />
</g>
<defs>
<filter
id="filter0_d_4914_58397"
x="4.5"
y="14"
width="11"
height="14"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<filter
id="filter1_d_4914_58397"
x="-2"
y="5"
width="15"
height="15"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<filter
id="filter2_d_4914_58397"
x="1"
y="9"
width="14"
height="14"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<filter
id="filter3_d_4914_58397"
x="1"
y="-2"
width="16"
height="16"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4914_58397" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4914_58397"
result="shape"
/>
</filter>
<linearGradient
id="paint0_linear_4914_58397"
x1="10"
y1="14"
x2="10"
y2="20"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#402309" />
<stop offset="1" stopColor="#713F12" />
</linearGradient>
<clipPath id="clip0_4914_58397">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,137 @@
export function SaplingIcon({ className }: { className?: string }) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4906_23816)">
<path
d="M4.39538 17.7403C7.25332 16.3563 13.2867 16.4291 15.5096 17.7403C16.6666 18.4227 17.2938 19.0069 17.6294 19.4103C17.8542 19.6806 17.651 20 17.2994 20H2.34215C2.151 20 1.9749 19.8835 2.01519 19.6967C2.09465 19.3281 2.53308 18.6421 4.39538 17.7403Z"
fill="url(#paint0_linear_4906_23816)"
/>
<g filter="url(#filter0_d_4906_23816)">
<path
d="M9.37762 9.80872C7.43627 7.16364 3.49903 8.13783 2.43836 8.66818C2.26159 9.02174 3.32907 8.80396 4.08851 10.8279C4.84794 12.8519 7.63503 13.1864 8.74087 12.0806C9.27121 11.5502 9.69141 10.2363 9.37762 9.80872Z"
fill="currentColor"
/>
</g>
<g filter="url(#filter1_d_4906_23816)">
<path
d="M9.03577 9.94522L9.99862 9.80223C9.99862 9.80223 10.0387 16.2908 10.9485 16.8179C11.8582 17.345 8.47933 17.1309 8.92618 16.8179C9.37303 16.505 9.03577 9.94522 9.03577 9.94522Z"
fill="currentColor"
/>
</g>
<g filter="url(#filter2_d_4906_23816)">
<path
d="M9.36235 10.1127C9.36235 5.78131 13.3229 4.41473 15.9395 4.00389C16.1892 3.96468 16.3818 4.22916 16.2961 4.46698C16.0763 5.07608 15.7914 6.09975 15.6717 7.46762C15.5019 9.40897 14.8971 10.3478 14.3613 10.962C13.3664 12.1026 9.36235 12.2341 9.36235 10.1127Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<filter
id="filter0_d_4906_23816"
x="-1.58107"
y="8.04684"
width="15.0664"
height="12.6571"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4906_23816"
result="shape"
/>
</filter>
<filter
id="filter1_d_4906_23816"
x="4.88525"
y="9.80223"
width="10.2195"
height="15.3357"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4906_23816"
result="shape"
/>
</filter>
<filter
id="filter2_d_4906_23816"
x="5.36235"
y="4"
width="14.9544"
height="15.7756"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4906_23816" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4906_23816"
result="shape"
/>
</filter>
<linearGradient
id="paint0_linear_4906_23816"
x1="10"
y1="16.7294"
x2="10"
y2="20"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.291667" stopColor="#402309" />
<stop offset="1" stopColor="#713F12" />
</linearGradient>
<clipPath id="clip0_4906_23816">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,44 @@
export function TwoTreesIcon({ className }: { className?: string }) {
return (
<svg
className={className}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_4906_58417)">
<path
d="M10 20V17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.7239 16.3L14.8274 13H15.1171C15.3061 12.9973 15.4901 12.9371 15.6464 12.8271C15.8027 12.717 15.9243 12.5619 15.9963 12.3808C16.0683 12.1998 16.0873 12.001 16.0512 11.8088C16.015 11.6167 15.9252 11.4398 15.7929 11.3L12.8965 8H13.0896C13.2868 8.01843 13.4847 7.97361 13.6565 7.87162C13.8282 7.76963 13.9656 7.6154 14.0499 7.42984C14.1342 7.24428 14.1614 7.03636 14.1278 6.83425C14.0942 6.63215 14.0014 6.44564 13.862 6.3L10 2L7.29699 5.0096C8.25755 5.0792 9.16726 5.48607 9.85673 6.16512C10.5124 6.81087 10.9224 7.65741 11.023 8.56163C11.6295 8.91846 12.136 9.42887 12.4856 10.0473C12.9492 10.8671 13.1066 11.8229 12.9286 12.7472C12.7507 13.6714 12.2496 14.5014 11.5168 15.0945C10.8041 15.6712 9.91568 15.9888 8.99914 15.999V18H17.0481C17.237 17.9973 17.4211 17.9371 17.5774 17.8271C17.7337 17.717 17.8553 17.5618 17.9273 17.3808C17.9992 17.1998 18.0183 17.001 17.9822 16.8088C17.946 16.6167 17.8562 16.4398 17.7239 16.3ZM7.99914 18V15.9991H6.00231V18H7.99914ZM4.97036 15.9991H5.00231L5.00231 18H2.95193C2.76296 17.9973 2.57892 17.9371 2.42263 17.8271C2.26635 17.717 2.14468 17.5618 2.07272 17.3808C2.00076 17.1998 1.98168 17.001 2.01783 16.8088C2.05398 16.6167 2.14377 16.4398 2.27609 16.3L3.01417 15.4591C3.59927 15.795 4.26325 15.9821 4.94587 15.9988C4.95403 15.999 4.9622 15.9991 4.97036 15.9991ZM3.69171 14.6872C4.08702 14.8804 4.52307 14.9881 4.97036 14.9991H8.93032C9.64375 15.0045 10.3365 14.7632 10.8877 14.3171C11.4389 13.871 11.8137 13.2485 11.9466 12.5581C12.0796 11.8678 11.9623 11.1534 11.6151 10.5395C11.268 9.9256 10.7131 9.45119 10.0472 9.19898V8.99898C10.0472 8.20331 9.7263 7.44023 9.15504 6.87761C8.58378 6.31499 7.80899 5.99891 7.00111 5.99891C6.77881 5.99891 6.55901 6.02285 6.34528 6.06926L6.13804 6.3C5.99864 6.44564 5.90584 6.63215 5.87222 6.83425C5.8386 7.03636 5.86579 7.24428 5.9501 7.42984C6.03441 7.6154 6.17176 7.76963 6.34354 7.87162C6.51533 7.97361 6.71323 8.01843 6.91043 8H7.10353L4.20706 11.3C4.07475 11.4398 3.98496 11.6167 3.94881 11.8088C3.91266 12.001 3.93174 12.1998 4.0037 12.3808C4.07566 12.5619 4.19732 12.717 4.35361 12.8271C4.5099 12.9371 4.69394 12.9973 4.88291 13H5.17255L3.69171 14.6872Z"
fill="currentColor"
/>
<path
d="M7 20L7 13.9814"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.0472 9V9.2C10.7131 9.4522 11.268 9.92661 11.6151 10.5405C11.9623 11.1544 12.0796 11.8687 11.9466 12.5591C11.8137 13.2494 11.4389 13.8719 10.8877 14.318C10.3365 14.7641 9.64374 15.0054 8.93032 15H4.97036C4.26546 14.9827 3.58847 14.7251 3.05487 14.2712C2.52126 13.8172 2.1641 13.195 2.0443 12.5106C1.92449 11.8263 2.04946 11.1222 2.3979 10.5185C2.74634 9.91474 3.29666 9.44875 3.95499 9.2V9C3.95499 8.20435 4.27592 7.44129 4.84718 6.87868C5.41843 6.31607 6.19323 6 7.00111 6C7.80899 6 8.58378 6.31607 9.15504 6.87868C9.7263 7.44129 10.0472 8.20435 10.0472 9Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_4906_58417">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

+34 -6
View File
@@ -20,6 +20,8 @@ import {
} from "./primitives/Select";
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "./primitives/Sheet";
import { TextArea } from "./primitives/TextArea";
import { DiscordIcon } from "@trigger.dev/companyicons";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
type FeedbackProps = {
button: ReactNode;
@@ -55,14 +57,15 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
<SheetContent size="sm">
<SheetHeader className="justify-between">Help & feedback</SheetHeader>
<SheetBody>
<Paragraph variant="small" className="mb-4">
Use this form to ask for help or give us feedback. We read every message and will get
<DiscordBanner />
<Paragraph variant="small" className="mb-4 border-t border-slate-800 pt-3">
Or use this form to ask for help or give us feedback. We read every message and will get
back to you as soon as we can.
</Paragraph>
<Form method="post" action="/resources/feedback" {...form.props}>
<Fieldset>
<Fieldset className="max-w-full">
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
<InputGroup>
<InputGroup className="max-w-full">
<Label>How can we help?</Label>
<SelectGroup>
<Select {...conform.input(feedbackType)} defaultValue={defaultValue}>
@@ -80,14 +83,14 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
</SelectGroup>
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
</InputGroup>
<InputGroup>
<InputGroup className="max-w-full">
<Label>Message</Label>
<TextArea {...conform.textarea(message)} />
<FormError id={message.errorId}>{message.error}</FormError>
</InputGroup>
<FormError>{form.error}</FormError>
<FormButtons
className="max-w-md"
className="w-full"
confirmButton={
<Button type="submit" variant="primary/medium">
Send
@@ -101,3 +104,28 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
</Sheet>
);
}
function DiscordBanner() {
return (
<a
href="https://discord.gg/nkqV9xBYWy"
target="_blank"
className="group mb-4 flex w-full items-center justify-between rounded-md border border-slate-600 bg-gradient-to-br from-blue-400/30 to-indigo-400/50 p-4 transition hover:border-indigo-400"
>
<div className="flex flex-col gap-y-2">
<DiscordIcon className="h-8 w-8" />
<h2 className="font-title text-2xl text-bright transition group-hover:text-white">
Join the Trigger.dev
<br />
Discord community
</h2>
<Paragraph variant="small">
Get help or answer questions from the Trigger.dev community.
</Paragraph>
</div>
<div className="h-full">
<ChevronRightIcon className="h-5 w-5 text-slate-400 transition group-hover:translate-x-1 group-hover:text-indigo-400" />
</div>
</a>
);
}
@@ -1,4 +1,6 @@
import { Link } from "@remix-run/react";
import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
import { Link, useSearchParams } from "@remix-run/react";
import invariant from "tiny-invariant";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
@@ -8,6 +10,7 @@ import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
import { jobTestPath } from "~/utils/pathBuilder";
import { Feedback } from "../Feedback";
import { CodeBlock } from "../code/CodeBlock";
import { InlineCode } from "../code/InlineCode";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
@@ -23,132 +26,199 @@ import {
ClientTabsTrigger,
} from "../primitives/ClientTabs";
import { ClipboardField } from "../primitives/ClipboardField";
import { Header2 } from "../primitives/Headers";
import { Header1, Header2 } from "../primitives/Headers";
import { NamedIcon } from "../primitives/NamedIcon";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
import { TextLink } from "../primitives/TextLink";
import integrationButton from "./integration-button.png";
import selectEnvironment from "./select-environment.png";
import selectExample from "./select-example.png";
import gradientBackground from "~/assets/images/gradient-background.png";
const existingProjectValue = "use-existing-project";
const newProjectValue = "create-new-next-app";
export function HowToSetupYourProject() {
const devEnvironment = useDevEnvironment();
const appOrigin = useAppOrigin();
return (
<>
<StepNumber stepNumber="1" title="Run the CLI 'init' command in a Next.js project" />
<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"
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`npx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`pnpm dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`yarn dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`yarn dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
</ClientTabs>
<Paragraph spacing>
Youll notice a new folder in your project called 'jobs'. Weve added a very simple
example Job in <InlineCode>examples.ts</InlineCode> to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Next.js app" />
<StepContentContainer>
<Paragraph>Ensure your app is running locally.</Paragraph>
<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 run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`pnpm run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`yarn run dev`} />
</ClientTabsContent>
</ClientTabs>
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<Paragraph spacing>
In a <strong className="text-bright">separate terminal window or tab</strong> run:
</Paragraph>
<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={`npx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
</ClientTabs>
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
<InlineCode>--port 3001</InlineCode> to the end.
</Paragraph>
<Paragraph spacing variant="small">
You should leave the <InlineCode>dev</InlineCode> command running when you're developing.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="4" title="Check for Jobs" />
<StepContentContainer>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example Job in the list.
</Paragraph>
<Button
variant="primary/medium"
className="mt-4"
LeadingIcon="refresh"
onClick={() => window.location.reload()}
const [searchQuery, setSearchQuery] = useSearchParams();
const selectedValue = searchQuery.get("selectedValue");
invariant(devEnvironment, "devEnvironment is required");
return (
<div
className="-ml-4 -mt-4 h-full w-[calc(100%+32px)] bg-cover bg-no-repeat pt-20"
style={{ backgroundImage: `url("${gradientBackground}")` }}
>
<div className="mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<Header1 spacing className="text-bright">
Get setup in {selectedValue === newProjectValue ? "5" : "2"} minutes
</Header1>
<Feedback
button={
<Button variant="secondary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
I'm stuck!
</Button>
}
defaultValue="help"
/>
</div>
<RadioGroup
className="mb-4 flex gap-x-2"
onValueChange={(value) => setSearchQuery({ selectedValue: value })}
>
Refresh
</Button>
</StepContentContainer>
</>
<RadioGroupItem
label="Use an existing Next.js project"
description="Use Trigger.dev in an existing Next.js project in less than 2 mins."
value={existingProjectValue}
checked={selectedValue === existingProjectValue}
variant="icon"
data-action={existingProjectValue}
icon={<NamedIcon className="h-12 w-12 text-green-600" name={"tree"} />}
/>
<RadioGroupItem
label="Create a new Next.js project"
description="This is the quickest way to try out Trigger.dev in a new Next.js project and takes 5 mins."
value={newProjectValue}
checked={selectedValue === newProjectValue}
variant="icon"
data-action={newProjectValue}
icon={<NamedIcon className="h-8 w-8 text-green-600" name={"sapling"} />}
/>
</RadioGroup>
{selectedValue && (
<>
{selectedValue === newProjectValue ? (
<>
<StepNumber stepNumber="1" title="Create a new Next.js project" />
<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={`npx create-next-app@latest`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm create next-app`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn create next-app`}
/>
</ClientTabsContent>
</ClientTabs>
<Paragraph spacing variant="small">
Trigger.dev works with either the Pages or App Router configuration.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Navigate to your new Next.js project" />
<StepContentContainer>
<Paragraph spacing>
You have now created a new Next.js project. Lets <InlineCode>cd</InlineCode>{" "}
into it using the project name you just provided:
</Paragraph>
<ClipboardField
value={"cd [replace with your project name]"}
variant={"primary/medium"}
></ClipboardField>
</StepContentContainer>
<StepNumber
stepNumber="3"
title="Run the CLI 'init' command in your new Next.js project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="4" title="Run your Next.js app" />
<StepContentContainer>
<NextDevCommand />
</StepContentContainer>
<StepNumber stepNumber="5" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="6" title="Check for Jobs" />
<StepContentContainer>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example Job in the
list.
</Paragraph>
<Button
variant="primary/medium"
className="mt-4"
LeadingIcon="refresh"
onClick={() => window.location.reload()}
>
Refresh
</Button>
</StepContentContainer>
</>
) : (
<>
<StepNumber
stepNumber="1"
title="Run the CLI 'init' command in an existing Next.js project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
to help you get started.
</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run your Next.js app" />
<StepContentContainer>
<NextDevCommand />
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStep />
</StepContentContainer>
<StepNumber stepNumber="4" title="Check for Jobs" />
<StepContentContainer>
<Paragraph>
Once you've run the CLI command, click Refresh to view your example Job in the
list.
</Paragraph>
<Button
variant="primary/medium"
className="mt-4"
LeadingIcon="refresh"
onClick={() => window.location.reload()}
>
Refresh
</Button>
</StepContentContainer>
</>
)}
</>
)}
</div>
</div>
);
}
@@ -391,3 +461,112 @@ export function HowToUseApiKeysAndEndpoints() {
function StepContentContainer({ children }: { children: React.ReactNode }) {
return <div className="mb-6 ml-9 mt-1">{children}</div>;
}
function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey: string }) {
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"
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`npx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`pnpm dlx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
secure={`yarn dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
value={`yarn dlx @trigger.dev/cli@latest init -k ${apiKey} -t ${appOrigin}`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
function NextDevCommand() {
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 run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`pnpm run dev`} />
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField variant="primary/medium" className="mb-4" value={`yarn run dev`} />
</ClientTabsContent>
</ClientTabs>
);
}
function TriggerDevCommand() {
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={`npx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`pnpm dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
className="mb-4"
value={`yarn dlx @trigger.dev/cli@latest dev`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
function TriggerDevStep() {
return (
<>
<Paragraph spacing>
In a <span className="text-amber-400">separate terminal window or tab</span> run:
</Paragraph>
<TriggerDevCommand />
<Paragraph spacing variant="small">
If youre not running on port 3000 you can specify the port by adding{" "}
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
</Paragraph>
<Paragraph spacing variant="small">
You should leave the <InlineCode variant="extra-small">dev</InlineCode> command running when
you're developing.
</Paragraph>
</>
);
}
+12 -2
View File
@@ -20,6 +20,7 @@ import { useProject } from "~/hooks/useProject";
import { useOrganization } from "~/hooks/useOrganizations";
import { JobRunStatus } from "~/models/job.server";
import { cn } from "~/utils/cn";
import { Badge } from "../primitives/Badge";
export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
const organization = useOrganization();
@@ -44,7 +45,10 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
return (
<TableRow
key={job.id}
className={cn(job.hasIntegrationsRequiringAction && "bg-rose-500/30")}
className={cn(
(job.hasIntegrationsRequiringAction && "bg-rose-500/20") ||
(job.lastRun === undefined && "bg-green-500/20")
)}
>
<TableCell to={path}>
<span className="flex items-center gap-2">
@@ -141,7 +145,13 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
<LabelValueStack label={"Never run"} value={""} />
)}
</TableCell>
<TableCellChevron to={path} />
<TableCellChevron to={path}>
{job.lastRun === undefined && (
<Badge className="mr-4" variant="green">
New Job!
</Badge>
)}
</TableCellChevron>
</TableRow>
);
})
@@ -6,12 +6,18 @@ const variants = {
"grid place-items-center rounded-full px-2 h-5 tracking-wider text-xxs bg-slate-700 text-bright uppercase whitespace-nowrap",
outline:
"grid place-items-center rounded-sm px-1 h-5 tracking-wider text-xxs border border-dimmed text-dimmed uppercase whitespace-nowrap",
green:
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider outline-offset-1 outline outline-1 outline-green-600 text-xxs bg-green-500 text-slate-900 uppercase whitespace-nowrap",
};
type BadgeProps = React.HTMLAttributes<HTMLDivElement> & {
variant?: keyof typeof variants;
};
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
return <div className={cn(variants[variant], className)} {...props} />;
export function Badge({ className, variant = "default", children, ...props }: BadgeProps) {
return (
<div className={cn(variants[variant], className)} {...props}>
<span className="-mb-0.5">{children}</span>
</div>
);
}
@@ -1,5 +1,5 @@
import { cn } from "~/utils/cn";
import { Paragraph, ParagraphVariant } from "./Paragraph";
import { Paragraph } from "./Paragraph";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
import { SimpleTooltip } from "./Tooltip";
@@ -58,6 +58,9 @@ import { WebhookIcon } from "~/assets/icons/WebhookIcon";
import { cn } from "~/utils/cn";
import { LogoIcon } from "../LogoIcon";
import { Spinner } from "./Spinner";
import { SaplingIcon } from "~/assets/icons/SaplingIcon";
import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon";
import { OneTreeIcon } from "~/assets/icons/OneTreeIcon";
const icons = {
account: (className: string) => <UserCircleIcon className={cn("text-slate-400", className)} />,
@@ -128,6 +131,7 @@ const icons = {
property: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
"qr-code": (className: string) => <QrCodeIcon className={cn("text-amber-400", className)} />,
refresh: (className: string) => <ArrowPathIcon className={cn("text-bright", className)} />,
sapling: (className: string) => <SaplingIcon className={cn("text-green-500", className)} />,
search: (className: string) => <MagnifyingGlassIcon className={cn("text-dimmed", className)} />,
settings: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
spinner: (className: string) => <Spinner className={className} color="blue" />,
@@ -135,6 +139,8 @@ const icons = {
star: (className: string) => <StarIcon className={cn("text-yellow-500", className)} />,
stop: (className: string) => <StopIcon className={cn("text-rose-500", className)} />,
team: (className: string) => <UserGroupIcon className={cn("text-blue-500", className)} />,
tree: (className: string) => <OneTreeIcon className={cn("text-green-500", className)} />,
trees: (className: string) => <TwoTreesIcon className={cn("text-green-500", className)} />,
trigger: (className: string) => <BoltIcon className={cn("text-orange-500", className)} />,
user: (className: string) => <UserIcon className={cn("text-blue-600", className)} />,
warning: (className: string) => (
@@ -11,12 +11,14 @@ const variants = {
label: "text-sm text-bright mt-0.5 select-none",
description: "text-dimmed",
inputPosition: "mt-1",
icon: "w-8 h-8 mb-2",
},
simple: {
button: "w-fit pr-4 data-[disabled]:opacity-70",
label: "text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-1",
icon: "w-8 h-8 mb-2",
},
"button/small": {
button:
@@ -24,6 +26,7 @@ const variants = {
label: "text-sm text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-0",
icon: "w-8 h-8 mb-2",
},
button: {
button:
@@ -31,6 +34,7 @@ const variants = {
label: "text-bright select-none",
description: "text-dimmed",
inputPosition: "mt-1",
icon: "w-8 h-8 mb-2",
},
description: {
button:
@@ -38,6 +42,15 @@ const variants = {
label: "text-bright font-semibold -mt-1 text-left",
description: "text-dimmed -mt-0 text-left",
inputPosition: "mt-0",
icon: "w-8 h-8 mb-2",
},
icon: {
button:
"w-full p-2.5 pb-4 hover:bg-slate-850 transition data-[disabled]:opacity-70 data-[state=checked]:bg-slate-850 border-slate-800 border rounded-sm",
label: "text-bright font-semibold -mt-1 text-left",
description: "text-dimmed -mt-0 text-left",
inputPosition: "mt-0",
icon: "mb-3",
},
};
@@ -57,53 +70,60 @@ type RadioGroupItemProps = Omit<
description?: string;
badges?: string[];
className?: string;
icon?: React.ReactNode;
};
export const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
RadioGroupItemProps
>(({ className, children, variant = "simple", label, description, badges, ...props }, ref) => {
const variation = variants[variant];
>(
(
{ className, children, variant = "simple", label, description, badges, icon, ...props },
ref
) => {
const variation = variants[variant];
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"group flex cursor-pointer items-start gap-x-2 transition",
variation.button,
className
)}
{...props}
>
<div
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-sm border border-slate-700 ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
variation.inputPosition
"group flex cursor-pointer items-start gap-x-2 transition",
variation.button,
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex h-full w-full items-center justify-center bg-indigo-700">
<Circle className="h-1.5 w-1.5 fill-white text-white" />
</RadioGroupPrimitive.Indicator>
</div>
<div>
<div className="flex items-center gap-x-2">
<label htmlFor={props.id} className={cn("cursor-pointer", variation.label)}>
{label}
</label>
{badges && (
<span className="-mr-2 flex gap-x-1.5">
{badges.map((badge) => (
<Badge key={badge}>{badge}</Badge>
))}
</span>
<div
className={cn(
"aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-sm border border-slate-700 ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
variation.inputPosition
)}
>
<RadioGroupPrimitive.Indicator className="flex h-full w-full items-center justify-center bg-indigo-700">
<Circle className="h-1.5 w-1.5 fill-white text-white" />
</RadioGroupPrimitive.Indicator>
</div>
<div className={cn(icon ? "flex h-full flex-col justify-end" : "")}>
{variant === "icon" && <div className={variation.icon}>{icon}</div>}
<div className="flex items-center gap-x-2">
<label htmlFor={props.id} className={cn("cursor-pointer", variation.label)}>
{label}
</label>
{badges && (
<span className="-mr-2 flex gap-x-1.5">
{badges.map((badge) => (
<Badge key={badge}>{badge}</Badge>
))}
</span>
)}
</div>
{(variant === "description" || variant === "icon") && (
<Paragraph variant="small" className={cn("mt-0.5", variation.description)}>
{description}
</Paragraph>
)}
</div>
{variant === "description" && (
<Paragraph variant="small" className={cn("mt-0.5", variation.description)}>
{description}
</Paragraph>
)}
</div>
</RadioGroupPrimitive.Item>
);
});
</RadioGroupPrimitive.Item>
);
}
);
@@ -51,7 +51,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-indigo-900 opacity-100 border-l border-y border-slate-800",
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-y border-slate-800",
{
variants: {
position: {
@@ -2,6 +2,7 @@ import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { ReactNode, forwardRef } from "react";
import { cn } from "~/utils/cn";
import { Badge } from "./Badge";
type TableProps = {
containerClassName?: string;
@@ -172,12 +173,14 @@ export const TableCellChevron = forwardRef<
{
className?: string;
to?: string;
children?: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
>(({ className, to, onClick }, ref) => {
>(({ className, to, children, onClick }, ref) => {
return (
<TableCell className={className} to={to} onClick={onClick} ref={ref} alignment="right">
<ChevronRightIcon className="h-4 w-4 text-slate-700 transition group-hover:text-bright" />
{children}
<ChevronRightIcon className="h-4 w-4 text-dimmed transition group-hover:text-bright" />
</TableCell>
);
});
@@ -325,7 +325,7 @@ function RerunPopover({
<Form method="post" action={`/resources/runs/${runId}/rerun`} {...form.props}>
<input {...conform.input(successRedirect, { type: "hidden" })} defaultValue={runsPath} />
{environmentType === "PRODUCTION" && (
<Callout variant="warning">
<Callout variant="warning" className="mb-2">
This will rerun this Job in your Production environment.
</Callout>
)}
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/react";
import { withDesign } from "storybook-addon-designs";
import { Badge } from "../primitives/Badge";
const meta: Meta = {
title: "Primitives/Badges",
decorators: [withDesign],
};
export default meta;
type Story = StoryObj<typeof BadgesExample>;
export const Basic: Story = {
render: () => <BadgesExample />,
};
function BadgesExample() {
return (
<div className="flex flex-col items-start gap-y-8 p-8">
<Badge>Default</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="green">Green</Badge>
</div>
);
}
@@ -3,6 +3,7 @@ import { withDesign } from "storybook-addon-designs";
import { useState } from "react";
import { Button } from "../primitives/Buttons";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
import { NamedIcon } from "../primitives/NamedIcon";
const meta: Meta = {
title: "Primitives/RadioGroup",
@@ -42,6 +43,14 @@ function RadioGroupExample() {
value={"5"}
variant="description"
/>
<RadioGroupItem
id="r7"
label="This is an icon label"
description="This is a description"
value={"6"}
variant="icon"
icon={<NamedIcon name="tree" className="h-8 w-8" />}
/>
</RadioGroup>
</form>
</div>
+2
View File
@@ -20,6 +20,8 @@ const EnvironmentSchema = z.object({
.default(process.env.NODE_ENV),
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
POSTHOG_PROJECT_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_KEY: z.string().optional(),
TELEMETRY_TRIGGER_API_URL: z.string().optional(),
HIGHLIGHT_PROJECT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
+99 -1
View File
@@ -1,4 +1,6 @@
import type {
CronItem,
CronItemOptions,
Job as GraphileJob,
Runner as GraphileRunner,
JobHelpers,
@@ -7,7 +9,7 @@ import type {
TaskList,
TaskSpec,
} from "graphile-worker";
import { run as graphileRun } from "graphile-worker";
import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
@@ -18,6 +20,13 @@ export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
}
const RawCronPayloadSchema = z.object({
_cron: z.object({
ts: z.coerce.date(),
backfilled: z.boolean(),
}),
});
const GraphileJobSchema = z.object({
id: z.coerce.string(),
queue_name: z.string().nullable(),
@@ -50,6 +59,19 @@ export type ZodTasks<TConsumerSchema extends MessageCatalogSchema> = {
};
};
type RecurringTaskPayload = {
ts: Date;
backfilled: boolean;
};
export type ZodRecurringTasks = {
[key: string]: {
pattern: string;
options?: CronItemOptions;
handler: (payload: RecurringTaskPayload, job: GraphileJob) => Promise<void>;
};
};
export type ZodWorkerEnqueueOptions = TaskSpec & {
tx?: PrismaClientOrTransaction;
};
@@ -59,6 +81,7 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
prisma: PrismaClient;
schema: TMessageCatalog;
tasks: ZodTasks<TMessageCatalog>;
recurringTasks?: ZodRecurringTasks;
};
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
@@ -66,6 +89,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
#prisma: PrismaClient;
#runnerOptions: RunnerOptions;
#tasks: ZodTasks<TMessageCatalog>;
#recurringTasks?: ZodRecurringTasks;
#runner?: GraphileRunner;
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
@@ -73,6 +97,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#prisma = options.prisma;
this.#runnerOptions = options.runnerOptions;
this.#tasks = options.tasks;
this.#recurringTasks = options.recurringTasks;
}
public async initialize(): Promise<boolean> {
@@ -84,9 +109,12 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
runnerOptions: this.#runnerOptions,
});
const parsedCronItems = parseCronItems(this.#createCronItemsFromRecurringTasks());
this.#runner = await graphileRun({
...this.#runnerOptions,
taskList: this.#createTaskListFromTasks(),
parsedCronItems,
});
if (!this.#runner) {
@@ -192,9 +220,38 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
taskList[key] = task;
}
for (const [key] of Object.entries(this.#recurringTasks ?? {})) {
const task: Task = (payload, helpers) => {
return this.#handleRecurringTask(key, payload, helpers);
};
taskList[key] = task;
}
return taskList;
}
#createCronItemsFromRecurringTasks() {
const cronItems: CronItem[] = [];
if (!this.#recurringTasks) {
return cronItems;
}
for (const [key, task] of Object.entries(this.#recurringTasks)) {
const cronItem: CronItem = {
pattern: task.pattern,
identifier: key,
task: key,
options: task.options,
};
cronItems.push(cronItem);
}
return cronItems;
}
async #handleMessage<K extends keyof TMessageCatalog>(
typeName: K,
rawPayload: unknown,
@@ -226,4 +283,45 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
await task.handler(payload, job);
}
async #handleRecurringTask(
typeName: string,
rawPayload: unknown,
helpers: JobHelpers
): Promise<void> {
const job = helpers.job;
logger.debug("Received recurring task, calling handler", {
type: String(typeName),
payload: rawPayload,
job,
});
const recurringTask = this.#recurringTasks?.[typeName];
if (!recurringTask) {
throw new Error(`No recurring task for message type: ${String(typeName)}`);
}
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) {
throw new Error(
`Failed to parse recurring task payload: ${JSON.stringify(parsedPayload.error)}`
);
}
const payload = parsedPayload.data;
try {
await recurringTask.handler(payload._cron, job);
} catch (error) {
logger.error("Failed to handle recurring task", {
error,
payload,
});
throw error;
}
}
}
@@ -1,13 +1,18 @@
import { ArrowUpIcon } from "@heroicons/react/24/solid";
import { LoaderArgs } from "@remix-run/server-runtime";
import { GitHubLightIcon, OpenAILightIcon, ResendIcon } from "@trigger.dev/companyicons";
import { CalendarDaysIcon, ClockIcon, SlackIcon } from "lucide-react";
import useWindowSize from "react-use/lib/useWindowSize";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import Confetti from "react-confetti";
import { HowToSetupYourProject } from "~/components/helpContent/HelpContentText";
import { JobsTable } from "~/components/jobs/JobsTable";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { Callout } from "~/components/primitives/Callout";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Input } from "~/components/primitives/Input";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import {
PageHeader,
PageInfoGroup,
@@ -17,24 +22,19 @@ import {
PageTitleRow,
} from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { TextLink } from "~/components/primitives/TextLink";
import { useFilterJobs } from "~/hooks/useFilterJobs";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { JobListPresenter } from "~/presenters/JobListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import useWindowSize from "react-use/lib/useWindowSize";
import {
docsPath,
ProjectParamSchema,
projectIntegrationsPath,
trimTrailingSlash,
ProjectParamSchema,
} from "~/utils/pathBuilder";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { requireUserId } from "~/services/session.server";
import { JobListPresenter } from "~/presenters/JobListPresenter.server";
import { TextLink } from "~/components/primitives/TextLink";
import { GitHubLightIcon, OpenAILightIcon, ResendIcon } from "@trigger.dev/companyicons";
import { ClockIcon, CalendarDaysIcon, SlackIcon } from "lucide-react";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -101,76 +101,51 @@ export default function Page() {
"rgb(217 70 239)",
]}
/> */}
<Help defaultOpen={jobs.length === 0}>
<Help>
{(open) => (
<div className={cn("grid h-fit gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div>
{jobs.length > 0 && jobs.some((j) => j.hasIntegrationsRequiringAction) && (
<Callout
variant="error"
to={projectIntegrationsPath(organization, project)}
className="mb-2"
>
Some of your Jobs have Integrations that have not been configured.
</Callout>
)}
<div className="mb-2 flex items-center justify-between gap-x-2">
{jobs.length === 0 ? (
<Header2>Jobs</Header2>
) : (
<Input
placeholder="Search Jobs"
variant="tertiary"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
)}
<HelpTrigger title="How do I add Trigger.dev to my Next.js app?" />
</div>
{jobs.length === 0 ? (
<div>
<div
className={
"flex w-full flex-col justify-center gap-x-4 rounded-md border border-dashed border-indigo-800 px-5 py-8"
}
>
<Paragraph variant="small">Your Jobs will appear here.</Paragraph>
</div>
</div>
) : (
<div className={cn("grid gap-4", open ? "h-full grid-cols-2" : " h-full grid-cols-1")}>
<div className="h-full">
{jobs.length > 0 ? (
<>
{jobs.some((j) => j.hasIntegrationsRequiringAction) && (
<Callout
variant="error"
to={projectIntegrationsPath(organization, project)}
className="mb-2"
>
Some of your Jobs have Integrations that have not been configured.
</Callout>
)}
<div className="mb-2 flex flex-col">
<Header2 spacing>Jobs</Header2>
<div className="flex w-full">
<Input
placeholder="Search Jobs"
variant="tertiary"
icon="search"
fullWidth={true}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<HelpTrigger title="Example Jobs and inspiration" />
</div>
</div>
<JobsTable
jobs={filteredItems}
noResultsText={`No Jobs match ${filterText}. Try a different search
query.`}
query.`}
/>
{jobs.length === 1 ? (
<>
<Callout
variant="docs"
to={docsPath("documentation/guides/create-a-job")}
className="my-3"
>
Create your first Job in code
</Callout>
<ExampleJobs />
</>
) : (
<Callout
variant="docs"
to={docsPath("documentation/guides/create-a-job")}
className="my-3"
>
Create another Job
</Callout>
{jobs.length === 1 && jobs.every((r) => r.lastRun === undefined) && (
<RunYourJobPrompt />
)}
</>
) : (
<HowToSetupYourProject />
)}
</div>
<HelpContent title="How to add Trigger.dev to your Next.js app (3 mins)">
<HowToSetupYourProject />
<HelpContent title="Example Jobs and inspiration">
<ExampleJobs />
</HelpContent>
</div>
)}
@@ -180,69 +155,155 @@ export default function Page() {
);
}
function ExampleJobs() {
function RunYourJobPrompt() {
return (
<div className="mt-6 flex w-full flex-col gap-y-2 rounded bg-slate-900 p-4">
<Header2 className="text-slate-300">Example Jobs</Header2>
<Paragraph variant="small">
If you want more inspiration or just want to dig into the code of a Job, check out our{" "}
<TextLink href="https://github.com/triggerdotdev/examples">examples repo</TextLink>.
<div className="mt-2 flex w-full gap-x-2 rounded border border-slate-800 bg-slate-900 p-4 pl-6">
<ArrowUpIcon className="h-5 w-5 animate-bounce text-green-500" />
<Paragraph variant="small" className="text-green-500">
Your Job is ready to run! Click it to run it now.
</Paragraph>
<div className="h-[1px] w-full bg-slate-800" />
<div className="flex gap-1.5">
<ClockIcon className="h-4 w-4 pt-0.5 text-slate-100" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/delays">
Delays
</TextLink>{" "}
- Using delays inside Jobs
</Paragraph>
</div>
<div className="flex gap-1.5">
<CalendarDaysIcon className="h-4 w-4 pt-0.5 text-slate-100" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/scheduled">
Scheduled
</TextLink>{" "}
- Interval and cron scheduled Jobs
</Paragraph>
</div>
<div className="flex gap-1.5">
<GitHubLightIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/github">
GitHub
</TextLink>{" "}
- When a new GitHub issue is opened it adds a Bug label to it.
</Paragraph>
</div>
<div className="flex gap-1.5">
<OpenAILightIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/openai">
OpenAI
</TextLink>{" "}
- Generate images and jokes from a prompt
</Paragraph>
</div>
<div className="flex gap-1.5">
<ResendIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/resend">
Resend
</TextLink>{" "}
- Sends an email by submitting a form in the Next.js app
</Paragraph>
</div>{" "}
<div className="flex gap-1.5">
<SlackIcon className="ml-0.5 h-4 w-4 pt-0.5" />
<Paragraph variant="small">
<TextLink href="https://github.com/triggerdotdev/examples/tree/main/slack">
Slack
</TextLink>{" "}
- Sends a Slack message when an event is received
</Paragraph>
</div>
</div>
);
}
function ExampleJobs() {
return (
<>
<Header2 spacing>Video walk-through</Header2>
<Paragraph spacing variant="small">
Watch Matt, CEO of Trigger.dev create a GitHub issue reminder in Slack using Trigger.dev.
(10 mins)
</Paragraph>
<iframe
src="https://www.youtube.com/embed/uocBQt2HeQo?&showinfo=0&rel=0&modestbranding=1"
title="Trigger.dev explainer video"
width="400"
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"
/>
<Header2 spacing>How to create a Job</Header2>
<Paragraph variant="small" spacing>
Our docs are a great way to learn how to create Jobs with each type of Trigger, from
webhooks, to delays, to triggering Jobs on a schedule.{" "}
</Paragraph>
<a
href="https://trigger.dev/docs/documentation/guides/create-a-job"
className="mb-4 flex w-full items-center rounded border-b border-slate-800 py-2 transition hover:border-transparent hover:bg-slate-800"
>
<NamedIcon name={"external-link"} className={iconStyles} />
<Paragraph variant="small" className="font-semibold text-bright">
How to create a Job
</Paragraph>
</a>
<Header2 spacing>Check out some example Jobs in code</Header2>
<Paragraph spacing variant="small">
If you're looking for inspiration for your next Job, check out our{" "}
<TextLink href="https://github.com/triggerdotdev/examples">examples repo</TextLink>. Or jump
straight into an example repo from the list below:
</Paragraph>
<div className="flex w-full flex-col">
{examples.map((example) => (
<a
href={example.codeLink}
key={example.title}
className="flex w-full items-center rounded border-b border-slate-800 py-2 transition hover:border-transparent hover:bg-slate-800"
>
{example.icon}
<Paragraph variant="small">
<span className="font-semibold text-bright">{example.title}</span> -{" "}
{example.description}
</Paragraph>
</a>
))}
</div>
</>
);
}
const iconStyles = "h-7 w-7 mr-2 pl-2 min-w-[28px]";
const examples = [
{
icon: <NamedIcon name={"clock"} className={iconStyles} />,
title: "Basic delay",
description: "Logs a message to the console, waits 5 minutes, and then logs another message.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/delays/src/jobs/delayJob.ts",
},
{
icon: <NamedIcon name="calendar" className={iconStyles} />,
title: "Basic interval",
description: "This Job runs every 60 seconds, starting 60 seconds after it is first indexed.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/interval.ts",
},
{
icon: <NamedIcon name="calendar" className={iconStyles} />,
title: "Cron scheduled interval",
description: "A scheduled Job which runs at 2:30pm every Monday.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/cronScheduled.ts",
},
{
icon: <NamedIcon name="openai" className={iconStyles} />,
title: "OpenAI text summarizer",
description:
"Summarizes a block of text, pulling out the most unique / helpful points using OpenAI.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/openai-text-summarizer/src/jobs/textSummarizer.ts",
},
{
icon: <NamedIcon name="openai" className={iconStyles} />,
title: "Tell me a joke using OpenAI",
description: "Generates a random joke using OpenAI GPT 3.5.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/tellMeAJoke.ts",
},
{
icon: <NamedIcon name="openai" className={iconStyles} />,
title: "Generate a random image using OpenAI",
description: "Generates a random image of a hedgehog using OpenAI DALL-E.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/generateHedgehogImages.ts",
},
{
icon: <NamedIcon name="resend" className={iconStyles} />,
title: "Send an email using Resend",
description: "Send a basic email using Resend.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/resend/src/jobs/resendBasicEmail.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "GitHub issue reminder",
description: "Sends a Slack message if a GitHub issue is left for 24h.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/github-issue-reminder/jobs/githubIssue.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "Github new star alert in Slack",
description: "When a repo is starred, a message is sent to a Slack.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "Add a custom label to a GitHub issue",
description: "When a new GitHub issue is opened it adds a “Bug” label to it.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/onIssueOpened.ts",
},
{
icon: <NamedIcon name="github" className={iconStyles} />,
title: "GitHub new star alert",
description: "When a repo is starred a message is logged with the new Stargazers count.",
codeLink: "https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarAlert.ts",
},
{
icon: <NamedIcon name="slack" className={iconStyles} />,
title: "Send a Slack message",
description: "Sends a Slack message to a specific channel when an event is received.",
codeLink:
"https://github.com/triggerdotdev/examples/blob/main/slack/src/jobs/sendSlackMessage.ts",
},
];
@@ -0,0 +1,121 @@
import { conform, useForm, useInputEvent } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher } from "@remix-run/react";
import { InlineCode } from "~/components/code/InlineCode";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { FormError } from "~/components/primitives/FormError";
import { Header1, Header2 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Sheet,
SheetBody,
SheetContent,
SheetHeader,
SheetTrigger,
} from "~/components/primitives/Sheet";
import { TextLink } from "~/components/primitives/TextLink";
import { docsPath } from "~/utils/pathBuilder";
import { bodySchema } from "../resources.projects.$projectId.endpoint";
import { RuntimeEnvironment, RuntimeEnvironmentType } from "@trigger.dev/database";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { useRef, useState } from "react";
type FirstEndpointSheetProps = {
projectId: string;
environments: { id: string; type: RuntimeEnvironmentType }[];
};
export function FirstEndpointSheet({ projectId, environments }: FirstEndpointSheetProps) {
const setEndpointUrlFetcher = useFetcher();
const [form, { url, environmentId }] = useForm({
id: "new-endpoint-url",
lastSubmission: setEndpointUrlFetcher.data,
onValidate({ formData }) {
return parse(formData, { schema: bodySchema });
},
});
const loadingEndpointUrl = setEndpointUrlFetcher.state !== "idle";
return (
<Sheet>
<SheetTrigger>
<ButtonContent variant={"primary/medium"}>Add your first endpoint</ButtonContent>
</SheetTrigger>
<SheetContent size="lg">
<SheetHeader>
<div>
<Header1>Add your first endpoint</Header1>
<Paragraph variant="small">
We recommend you use{" "}
<TextLink href={docsPath("documentation/guides/cli")}>the CLI</TextLink> when working
in development.
</Paragraph>
</div>
</SheetHeader>
<SheetBody>
<setEndpointUrlFetcher.Form
method="post"
action={`/resources/projects/${projectId}/endpoint`}
{...form.props}
>
<InputGroup className="mb-4 max-w-none">
<Header2>Environment type</Header2>
<SelectGroup>
<Select name={"environmentId"} defaultValue={environments[0].id}>
<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>
<FormError id={environmentId.errorId}>{environmentId.error}</FormError>
</InputGroup>
<InputGroup className="max-w-none">
<Header2>Endpoint URL</Header2>
<div className="flex items-center">
<Input
className="rounded-r-none"
{...conform.input(url, { type: "url" })}
placeholder="URL for your Trigger API route"
/>
<Button
type="submit"
variant="primary/medium"
className="rounded-l-none"
disabled={loadingEndpointUrl}
LeadingIcon={loadingEndpointUrl ? "spinner-white" : undefined}
>
{loadingEndpointUrl ? "Saving" : "Save"}
</Button>
</div>
<FormError id={url.errorId}>{url.error}</FormError>
<FormError id={form.errorId}>{form.error}</FormError>
<Hint>
This is the URL of your Trigger API route, Typically this would be:{" "}
<InlineCode variant="extra-small">https://yourdomain.com/api/trigger</InlineCode>.
</Hint>
</InputGroup>
</setEndpointUrlFetcher.Form>
</SheetBody>
</SheetContent>
</Sheet>
);
}
@@ -7,7 +7,7 @@ import { EnvironmentLabel, environmentTitle } from "~/components/environments/En
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
import { ButtonContent } from "~/components/primitives/Buttons";
import { Button, ButtonContent } from "~/components/primitives/Buttons";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
@@ -39,6 +39,7 @@ import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
import { Badge } from "~/components/primitives/Badge";
import { FirstEndpointSheet } from "./FirstEndpointSheet";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -95,6 +96,13 @@ export default function Page() {
};
}, [selected, clients]);
const isAnyClientFullyConfigured = useMemo(() => {
return clients.some((client) => {
const { DEVELOPMENT, PRODUCTION } = client.endpoints;
return PRODUCTION.state === "configured" && DEVELOPMENT.state === PRODUCTION.state;
});
}, [clients]);
const organization = useOrganization();
const project = useProject();
@@ -119,7 +127,7 @@ export default function Page() {
<PageDescription>API Keys and endpoints for your environments.</PageDescription>
</PageHeader>
<PageBody>
<Help defaultOpen>
<Help defaultOpen={!isAnyClientFullyConfigured}>
{(open) => (
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div>
@@ -202,7 +210,12 @@ export default function Page() {
</div>
))
) : (
<Paragraph>You have no clients yet</Paragraph>
<>
<Paragraph>Add your first endpoint</Paragraph>
<Paragraph>
<FirstEndpointSheet projectId={project.id} environments={environments} />
</Paragraph>
</>
)}
</div>
{selectedEndpoint && (
@@ -8,7 +8,7 @@ import { ProjectsMenu } from "~/components/navigation/ProjectsMenu";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
import { analytics } from "~/services/analytics.server";
import { telemetry } from "~/services/telemetry.server";
import { requireUserId } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import { projectPath } from "~/utils/pathBuilder";
@@ -33,7 +33,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
});
}
analytics.project.identify({ project });
telemetry.project.identify({ project });
return typedjson({
project,
@@ -6,7 +6,7 @@ import invariant from "tiny-invariant";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { useOrganization } from "~/hooks/useOrganizations";
import { getOrganizationFromSlug } from "~/models/organization.server";
import { analytics } from "~/services/analytics.server";
import { telemetry } from "~/services/telemetry.server";
import { commitCurrentOrgSession, setCurrentOrg } from "~/services/currentOrganization.server";
import { requireUserId } from "~/services/session.server";
import { organizationPath } from "~/utils/pathBuilder";
@@ -25,7 +25,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
throw new Response("Not Found", { status: 404 });
}
analytics.organization.identify({ organization });
telemetry.organization.identify({ organization });
const session = await setCurrentOrg(organization.slug, request);
@@ -0,0 +1,71 @@
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 { 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);
const formData = await request.formData();
const submission = parse(formData, { schema: bodySchema });
if (!submission.value || submission.intent !== "submit") {
return json(submission);
}
try {
const environment = await prisma.runtimeEnvironment.findUnique({
include: {
organization: true,
project: true,
},
where: {
id: submission.value.environmentId,
},
});
if (!environment) {
submission.error.environmentId = "Environment not found";
return json(submission);
}
const service = new ValidateCreateEndpointService();
const result = await service.call({
url: submission.value.url,
environment,
});
return json(submission);
} catch (e) {
if (e instanceof CreateEndpointError) {
submission.error.url = e.message;
return json(submission);
}
if (e instanceof Error) {
submission.error.url = `${e.name}: ${e.message}`;
} else {
submission.error.url = "Unknown error";
}
return json(submission, { status: 400 });
}
}
@@ -1,364 +0,0 @@
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
import type { User } from "~/models/user.server";
class BehaviouralAnalytics {
client: PostHog | undefined = undefined;
constructor(apiKey?: string) {
if (!apiKey) {
console.log("No PostHog API key, so analytics won't track");
return;
}
this.client = new PostHog(apiKey, { host: "https://app.posthog.com" });
}
user = {
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
if (this.client === undefined) return;
this.client.identify({
distinctId: user.id,
properties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
isNewUser,
},
});
if (isNewUser) {
this.#capture({
userId: user.id,
event: "user created",
eventProperties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
},
});
}
},
};
organization = {
identify: ({ organization }: { organization: Organization }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "organization",
groupKey: organization.id,
properties: {
name: organization.title,
slug: organization.slug,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
});
},
new: ({
userId,
organization,
organizationCount,
}: {
userId: string;
organization: Organization;
organizationCount: number;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "organization created",
organizationId: organization.id,
eventProperties: {
id: organization.id,
slug: organization.slug,
title: organization.title,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
userProperties: {
organizationCount: organizationCount,
},
});
},
};
project = {
identify: ({ project }: { project: Project }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "project",
groupKey: project.id,
properties: {
name: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
new: ({
userId,
organizationId,
project,
}: {
userId: string;
organizationId: string;
project: Project;
}) => {
if (this.client === undefined) return;
this.#capture({
userId,
event: "project created",
organizationId,
eventProperties: {
id: project.id,
title: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
};
//todo Job
// workflow = {
// identify: ({ workflow }: { workflow: Workflow }) => {
// if (this.client === undefined) return;
// this.client.groupIdentify({
// groupType: "workflow",
// groupKey: workflow.id,
// properties: {
// name: workflow.title,
// slug: workflow.slug,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// });
// },
// new: ({
// userId,
// organizationId,
// workflow,
// workflowCount,
// }: {
// userId: string;
// organizationId: string;
// workflow: Workflow;
// workflowCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow created",
// organizationId: organizationId,
// jobId: workflow.id,
// eventProperties: {
// id: workflow.id,
// slug: workflow.slug,
// title: workflow.title,
// packageJson: workflow.packageJson,
// jsonSchema: workflow.jsonSchema,
// createdAt: workflow.createdAt,
// updatedAt: workflow.updatedAt,
// organizationId: workflow.organizationId,
// type: workflow.type,
// status: workflow.status,
// externalSourceId: workflow.externalSourceId,
// service: workflow.service,
// eventNames: workflow.eventNames,
// disabledAt: workflow.disabledAt,
// archivedAt: workflow.archivedAt,
// isArchived: workflow.isArchived,
// triggerTtlInSeconds: workflow.triggerTtlInSeconds,
// },
// userProperties: {
// workflowCount: workflowCount,
// },
// });
// },
// };
// workflowRun = {
// new: ({
// userId,
// organizationId,
// workflowId,
// workflowRun,
// environmentType,
// runCount,
// }: {
// userId: string;
// organizationId: string;
// workflowId: string;
// workflowRun: WorkflowRun;
// environmentType: string;
// runCount: number;
// }) => {
// if (this.client === undefined) return;
// this.#capture({
// userId,
// event: "workflow run created",
// eventProperties: {
// id: workflowRun.id,
// workflowId: workflowRun.workflowId,
// environmentId: workflowRun.environmentId,
// environmentType,
// eventRuleId: workflowRun.eventRuleId,
// eventId: workflowRun.eventId,
// error: workflowRun.error,
// status: workflowRun.status,
// attemptCount: workflowRun.attemptCount,
// createdAt: workflowRun.createdAt,
// updatedAt: workflowRun.updatedAt,
// startedAt: workflowRun.startedAt,
// finishedAt: workflowRun.finishedAt,
// timedOutAt: workflowRun.timedOutAt,
// timedOutReason: workflowRun.timedOutReason,
// isTest: workflowRun.isTest,
// },
// userProperties: {
// runCount: runCount,
// },
// organizationId: organizationId,
// jobId: workflowId,
// environmentId: workflowRun.environmentId,
// });
// },
// };
environment = {
identify: ({ environment }: { environment: RuntimeEnvironment }) => {
if (this.client === undefined) return;
this.client.groupIdentify({
groupType: "environment",
groupKey: environment.id,
properties: {
name: environment.slug,
slug: environment.slug,
organizationId: environment.organizationId,
createdAt: environment.createdAt,
updatedAt: environment.updatedAt,
},
});
},
};
telemetry = {
capture: ({
userId,
event,
properties,
organizationId,
environmentId,
}: {
userId: string;
event: string;
properties: Record<string | number, any>;
organizationId?: string;
environmentId?: string;
}) => {
this.#capture({
userId,
event,
eventProperties: properties,
organizationId,
environmentId,
});
},
};
#capture(event: CaptureEvent) {
if (this.client === undefined) return;
let groups: Record<string, string> = {};
if (event.organizationId) {
groups = {
...groups,
organization: event.organizationId,
};
}
if (event.projectId) {
groups = {
...groups,
project: event.projectId,
};
}
if (event.jobId) {
groups = {
...groups,
workflow: event.jobId,
};
}
if (event.environmentId) {
groups = {
...groups,
environment: event.environmentId,
};
}
let properties: Record<string, any> = {};
if (event.eventProperties) {
properties = {
...properties,
...event.eventProperties,
};
}
if (event.userProperties) {
properties = {
...properties,
$set: event.userProperties,
};
}
if (event.userOnceProperties) {
properties = {
...properties,
$set_once: event.userOnceProperties,
};
}
const eventData = {
distinctId: event.userId,
event: event.event,
properties,
groups,
};
this.client.capture(eventData);
}
}
type CaptureEvent = {
userId: string;
event: string;
organizationId?: string;
projectId?: string;
jobId?: string;
environmentId?: string;
eventProperties?: Record<string, any>;
userProperties?: Record<string, any>;
userOnceProperties?: Record<string, any>;
};
export const analytics = new BehaviouralAnalytics(env.POSTHOG_PROJECT_KEY);
+6
View File
@@ -1,6 +1,7 @@
import type { DeliverEmail } from "emails";
import { EmailClient } from "emails";
import type { SendEmailOptions } from "remix-auth-email-link";
import { redirect } from "remix-typedjson";
import { env } from "~/env.server";
import type { User } from "~/models/user.server";
import type { AuthUser } from "./authUser";
@@ -14,6 +15,11 @@ const client = new EmailClient({
});
export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): Promise<void> {
// Auto redirect when in development mode
if (env.NODE_ENV === "development") {
throw redirect(options.magicLink);
}
return client.send({
email: "magic_link",
to: options.emailAddress,
+63 -4
View File
@@ -13,6 +13,8 @@ import {
RegisterTriggerBodySchema,
RunJobBody,
RunJobResponseSchema,
ValidateResponse,
ValidateResponseSchema,
} from "@trigger.dev/core";
import { safeBodyFromResponse, safeParseBodyFromResponse } from "~/utils/json";
import { logger } from "./logger.server";
@@ -25,16 +27,15 @@ export class EndpointApiError extends Error {
}
}
// TODO: this should work with tunnelling
export class EndpointApi {
constructor(private apiKey: string, private url: string, private id: string) {}
constructor(private apiKey: string, private url: string) {}
async ping(): Promise<PongResponse> {
async ping(endpointId: string): Promise<PongResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-endpoint-id": this.id,
"x-trigger-endpoint-id": endpointId,
"x-trigger-action": "PING",
},
});
@@ -271,6 +272,64 @@ export class EndpointApi {
return HttpSourceResponseSchema.parse(anyBody);
}
async validate(): Promise<ValidateResponse> {
const response = await safeFetch(this.url, {
method: "POST",
headers: {
"x-trigger-api-key": this.apiKey,
"x-trigger-action": "VALIDATE",
},
});
if (!response) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}`,
};
}
if (response.status === 401) {
const body = await safeBodyFromResponse(response, ErrorWithStackSchema);
if (body) {
return {
ok: false,
error: body.message,
} as const;
}
return {
ok: false,
error: `Trigger API key is invalid`,
} as const;
}
if (!response.ok) {
return {
ok: false,
error: `Could not connect to endpoint ${this.url}. Status code: ${response.status}`,
};
}
const validateResponse = await safeParseBodyFromResponse(response, ValidateResponseSchema);
if (!validateResponse) {
return {
ok: false,
error: `Could not parse response from endpoint. Make sure it points to the correct URL (you might be missing /api/trigger)`,
};
}
if (!validateResponse.success) {
return {
ok: false,
error: `Endpoint ${this.url} responded with error: ${validateResponse.error.message}`,
};
}
return validateResponse.data;
}
}
async function safeFetch(url: string, options: RequestInit) {
@@ -34,9 +34,9 @@ export class CreateEndpointService {
}) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl, id);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const pong = await client.ping();
const pong = await client.ping(id);
if (!pong.ok) {
throw new CreateEndpointError("FAILED_PING", pong.error);
@@ -28,7 +28,7 @@ export class IndexEndpointService {
const endpoint = await findEndpoint(id);
// Make a request to the endpoint to fetch a list of jobs
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url, endpoint.slug);
const client = new EndpointApi(endpoint.environment.apiKey, endpoint.url);
const indexResponse = await client.indexEndpoint();
@@ -0,0 +1,44 @@
import { PrismaClient, prisma } from "~/db.server";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
export class RecurringEndpointIndexService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(ts: Date) {
// Find all production endpoints that haven't been indexed in the last 10 minutes
const currentTimestamp = ts.getTime();
const endpoints = await this.#prismaClient.endpoint.findMany({
where: {
environment: {
type: RuntimeEnvironmentType.PRODUCTION,
},
indexings: {
none: {
createdAt: {
gt: new Date(currentTimestamp - 10 * 60 * 1000),
},
},
},
},
});
logger.debug("Found endpoints that haven't been indexed in the last 10 minutes", {
count: endpoints.length,
});
// Enqueue each endpoint for indexing
for (const endpoint of endpoints) {
await workerQueue.enqueue("indexEndpoint", {
id: endpoint.id,
source: "INTERNAL",
});
}
}
}
@@ -0,0 +1,100 @@
import { customAlphabet } from "nanoid";
import { $transaction, prisma, PrismaClient } from "~/db.server";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { workerQueue } from "../worker.server";
import { CreateEndpointError } from "./createEndpoint.server";
import { EndpointApi } from "../endpointApi.server";
const indexingHookIdentifier = customAlphabet("0123456789abcdefghijklmnopqrstuvxyz", 10);
export class ValidateCreateEndpointService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ environment, url }: { environment: AuthenticatedEnvironment; url: string }) {
const endpointUrl = this.#normalizeEndpointUrl(url);
const client = new EndpointApi(environment.apiKey, endpointUrl);
const validationResult = await client.validate();
if (!validationResult.ok) {
throw new Error(validationResult.error);
}
try {
const result = await $transaction(this.#prismaClient, async (tx) => {
const endpoint = await tx.endpoint.upsert({
where: {
environmentId_slug: {
environmentId: environment.id,
slug: validationResult.endpointId,
},
},
create: {
environment: {
connect: {
id: environment.id,
},
},
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
slug: validationResult.endpointId,
url: endpointUrl,
indexingHookIdentifier: indexingHookIdentifier(),
},
update: {
url: endpointUrl,
},
});
// Kick off process to fetch the jobs for this endpoint
await workerQueue.enqueue(
"indexEndpoint",
{
id: endpoint.id,
source: "INTERNAL",
},
{ tx }
);
return endpoint;
});
return result;
} catch (error) {
if (error instanceof Error) {
throw new CreateEndpointError("FAILED_UPSERT", error.message);
} else {
throw new CreateEndpointError("FAILED_UPSERT", "Something went wrong");
}
}
}
// If the endpoint URL points to localhost, and the RUNTIME_PLATFORM is docker-compose, then we need to rewrite the host to host.docker.internal
// otherwise we shouldn't change anything
#normalizeEndpointUrl(url: string) {
if (env.RUNTIME_PLATFORM === "docker-compose") {
const urlObj = new URL(url);
if (urlObj.hostname === "localhost") {
urlObj.hostname = "host.docker.internal";
return urlObj.toString();
}
}
return url;
}
}
@@ -1,6 +1,6 @@
import type { EventDispatcher, EventRecord } from "@trigger.dev/database";
import type { EventFilter } from "@trigger.dev/core";
import { EventFilterSchema } from "@trigger.dev/core";
import { EventFilterSchema, eventFilterMatches } from "@trigger.dev/core";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { workerQueue } from "../worker.server";
@@ -124,29 +124,6 @@ export class EventMatcher {
}
public matches(filter: EventFilter) {
return patternMatches(this.event, filter);
return eventFilterMatches(this.event, filter);
}
}
function patternMatches(payload: any, pattern: any): boolean {
for (const [patternKey, patternValue] of Object.entries(pattern)) {
const payloadValue = payload[patternKey];
if (Array.isArray(patternValue)) {
if (patternValue.length > 0 && !patternValue.includes(payloadValue)) {
return false;
}
} else if (typeof patternValue === "object") {
if (Array.isArray(payloadValue)) {
if (!payloadValue.some((item) => patternMatches(item, patternValue))) {
return false;
}
} else {
if (!patternMatches(payloadValue, patternValue)) {
return false;
}
}
}
}
return true;
}
@@ -10,11 +10,11 @@ const supabase = new SupabaseManagement({
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
id: "on-new-todos",
name: "On New Todos",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
trigger: supabase.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {
},
@@ -33,11 +33,11 @@ const supabase = new SupabaseManagement({
});
new Job(client, {
id: "on-new-users",
name: "On New Users",
id: "on-new-todos",
name: "On New Todos",
version: "0.1.1",
trigger: supabase.onInsert({
table: "users",
trigger: supabase.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {
},
+2 -2
View File
@@ -1,5 +1,5 @@
import type { User } from "~/models/user.server";
import { analytics } from "./analytics.server";
import { telemetry } from "./telemetry.server";
export async function postAuthentication({
user,
@@ -10,5 +10,5 @@ export async function postAuthentication({
loginMethod: User["authenticationMethod"];
isNewUser: boolean;
}) {
analytics.user.identify({ user, isNewUser });
telemetry.user.identify({ user, isNewUser });
}
@@ -54,7 +54,7 @@ export class PerformRunExecutionService {
async #executePreprocessing(execution: FoundRunExecution) {
const { run } = execution;
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url, run.endpoint.slug);
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const startedAt = new Date();
@@ -189,7 +189,7 @@ export class PerformRunExecutionService {
return;
}
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url, run.endpoint.slug);
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const startedAt = new Date();
@@ -55,8 +55,7 @@ export class DeliverHttpSourceRequestService {
const clientApi = new EndpointApi(
httpSourceRequest.environment.apiKey,
httpSourceRequest.endpoint.url,
httpSourceRequest.endpoint.slug
httpSourceRequest.endpoint.url
);
const { response, events } = await clientApi.deliverHttpSourceRequest({
@@ -0,0 +1,239 @@
import { TriggerClient } from "@trigger.dev/sdk";
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import type { User } from "~/models/user.server";
type Options = {
postHogApiKey?: string;
trigger?: {
apiKey: string;
apiUrl: string;
};
};
class Telemetry {
#posthogClient: PostHog | undefined = undefined;
#triggerClient: TriggerClient | undefined = undefined;
constructor({ postHogApiKey, trigger }: Options) {
if (postHogApiKey) {
this.#posthogClient = new PostHog(postHogApiKey, { host: "https://app.posthog.com" });
} else {
console.log("No PostHog API key, so analytics won't track");
}
if (trigger) {
this.#triggerClient = new TriggerClient({
id: "triggerdotdev",
apiKey: trigger.apiKey,
apiUrl: trigger.apiUrl,
});
console.log("Created telemetry TriggerClient");
}
}
user = {
identify: ({ user, isNewUser }: { user: User; isNewUser: boolean }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.identify({
distinctId: user.id,
properties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
isNewUser,
},
});
if (isNewUser) {
this.#capture({
userId: user.id,
event: "user created",
eventProperties: {
email: user.email,
name: user.name,
authenticationMethod: user.authenticationMethod,
admin: user.admin,
createdAt: user.createdAt,
},
});
this.#triggerClient?.sendEvent({
name: "user.created",
payload: {
userId: user.id,
},
});
}
},
};
organization = {
identify: ({ organization }: { organization: Organization }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.groupIdentify({
groupType: "organization",
groupKey: organization.id,
properties: {
name: organization.title,
slug: organization.slug,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
});
},
new: ({
userId,
organization,
organizationCount,
}: {
userId: string;
organization: Organization;
organizationCount: number;
}) => {
if (this.#posthogClient === undefined) return;
this.#capture({
userId,
event: "organization created",
organizationId: organization.id,
eventProperties: {
id: organization.id,
slug: organization.slug,
title: organization.title,
createdAt: organization.createdAt,
updatedAt: organization.updatedAt,
},
userProperties: {
organizationCount: organizationCount,
},
});
},
};
project = {
identify: ({ project }: { project: Project }) => {
if (this.#posthogClient === undefined) return;
this.#posthogClient.groupIdentify({
groupType: "project",
groupKey: project.id,
properties: {
name: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
new: ({
userId,
organizationId,
project,
}: {
userId: string;
organizationId: string;
project: Project;
}) => {
if (this.#posthogClient === undefined) return;
this.#capture({
userId,
event: "project created",
organizationId,
eventProperties: {
id: project.id,
title: project.name,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
},
});
},
};
#capture(event: CaptureEvent) {
if (this.#posthogClient === undefined) return;
let groups: Record<string, string> = {};
if (event.organizationId) {
groups = {
...groups,
organization: event.organizationId,
};
}
if (event.projectId) {
groups = {
...groups,
project: event.projectId,
};
}
if (event.jobId) {
groups = {
...groups,
workflow: event.jobId,
};
}
if (event.environmentId) {
groups = {
...groups,
environment: event.environmentId,
};
}
let properties: Record<string, any> = {};
if (event.eventProperties) {
properties = {
...properties,
...event.eventProperties,
};
}
if (event.userProperties) {
properties = {
...properties,
$set: event.userProperties,
};
}
if (event.userOnceProperties) {
properties = {
...properties,
$set_once: event.userOnceProperties,
};
}
const eventData = {
distinctId: event.userId,
event: event.event,
properties,
groups,
};
this.#posthogClient.capture(eventData);
}
}
type CaptureEvent = {
userId: string;
event: string;
organizationId?: string;
projectId?: string;
jobId?: string;
environmentId?: string;
eventProperties?: Record<string, any>;
userProperties?: Record<string, any>;
userOnceProperties?: Record<string, any>;
};
export const telemetry = new Telemetry({
postHogApiKey: env.POSTHOG_PROJECT_KEY,
trigger:
env.TELEMETRY_TRIGGER_API_KEY && env.TELEMETRY_TRIGGER_API_URL
? {
apiKey: env.TELEMETRY_TRIGGER_API_KEY,
apiUrl: env.TELEMETRY_TRIGGER_API_URL,
}
: undefined,
});
@@ -45,7 +45,7 @@ export class InitializeTriggerService {
},
});
const clientApi = new EndpointApi(environment.apiKey, endpoint.url, endpoint.slug);
const clientApi = new EndpointApi(environment.apiKey, endpoint.url);
const registerMetadata = await clientApi.initializeTrigger(dynamicTrigger.slug, payload.params);
+27 -1
View File
@@ -6,6 +6,7 @@ import { env } from "~/env.server";
import { ZodWorker } from "~/platform/zodWorker.server";
import { sendEmail } from "./email.server";
import { IndexEndpointService } from "./endpoints/indexEndpoint.server";
import { RecurringEndpointIndexService } from "./endpoints/recurringEndpointIndex.server";
import { DeliverEventService } from "./events/deliverEvent.server";
import { InvokeDispatcherService } from "./events/invokeDispatcher.server";
import { integrationAuthRepository } from "./externalApis/integrationAuthRepository.server";
@@ -95,6 +96,31 @@ function getWorkerQueue() {
pollInterval: 1000,
},
schema: workerCatalog,
recurringTasks: {
// Run this every 5 minutes
autoIndexProductionEndpoints: {
pattern: "*/5 * * * *",
handler: async (payload, job) => {
const service = new RecurringEndpointIndexService();
await service.call(payload.ts);
},
},
// Run this every hour
purgeOldIndexings: {
pattern: "0 * * * *",
handler: async (payload, job) => {
// Delete indexings that are older than 7 days
await prisma.endpointIndex.deleteMany({
where: {
createdAt: {
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
});
},
},
},
tasks: {
"events.invokeDispatcher": {
maxAttempts: 3,
@@ -154,7 +180,7 @@ function getWorkerQueue() {
},
},
deliverHttpSourceRequest: {
maxAttempts: 5,
maxAttempts: 25,
handler: async (payload, job) => {
const service = new DeliverHttpSourceRequestService();
+1 -1
View File
@@ -1,4 +1,4 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Roboto+Mono:wght@300;400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Roboto+Mono:wght@300;400;500;600;700&display=swap&family=Poppins:wght@600&display=swap");
@tailwind base;
@tailwind components;
+2
View File
@@ -14,6 +14,7 @@
"start": "cross-env NODE_ENV=production node --max-old-space-size=8192 ./build/server.js",
"typecheck": "tsc --noEmit",
"db:seed": "node prisma/seed.js",
"db:seed:local": "ts-node prisma/seed.ts",
"generate:sourcemaps": "remix build --sourcemap",
"clean:sourcemaps": "run-s clean:sourcemaps:*",
"clean:sourcemaps:public": "rimraf ./build/**/*.map",
@@ -61,6 +62,7 @@
"@trigger.dev/companyicons": "^1.5.14",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"@uiw/react-codemirror": "^4.19.5",
"class-variance-authority": "^0.5.2",
"clsx": "^1.2.1",
+13 -2
View File
@@ -12,8 +12,19 @@ module.exports = {
ignoredRouteFiles: ["**/.*"],
devServerPort: 8002,
serverModuleFormat: "cjs",
serverDependenciesToBundle: ["marked", "axios", "@trigger.dev/core", "emails", "highlight.run"],
serverDependenciesToBundle: [
"marked",
"axios",
"@trigger.dev/core",
"@trigger.dev/sdk",
"emails",
"highlight.run",
],
watchPaths: async () => {
return ["../../packages/core/src/**/*", "../../packages/emails/src/**/*"];
return [
"../../packages/core/src/**/*",
"../../packages/trigger-sdk/src/**/*",
"../../packages/emails/src/**/*",
];
},
};
+1
View File
@@ -76,6 +76,7 @@ module.exports = {
fontFamily: {
sans: ["Inter", "sans-serif"],
mono: ["Roboto Mono", "monospace"],
title: ["Poppins", "sans-serif"]
},
fontSize: {
xxs: [
+262
View File
@@ -0,0 +1,262 @@
---
title: Using Manual Setup
description: How to Manually Initialize Trigger.dev in your Next.js project
---
<Accordion defaultOpen title="Don't have a Next.js project yet to add Trigger.dev to? No problem, you can complete the Manual Setup using a blank Next.js project:">
Create a blank project by running the `create-next-app` command in your terminal:
```bash
npx create-next-app@latest
```
Trigger.dev works with either the Pages or App Router configuration.
</Accordion>
## Installing Required Packages
To begin, install the necessary packages in your Next.js project directory. You can choose one of the following package managers:
<CodeGroup>
```bash npm
npm i @trigger.dev/sdk @trigger-dev/nextjs
```
```bash pnpm
pnpm install @trigger.dev/sdk @trigger-dev/nextjs
```
```bash yarn
yarn add @trigger.dev/sdk @trigger-dev/nextjs
```
</CodeGroup>
<br />
<Note>Ensure that you execute this command within a Next.js project.</Note>
## Obtaining the Development API Key
To locate your development API key, login to the [Trigger.dev
dashboard](https://cloud.trigger.dev) and select the Project you want to
connect to. Then click on the Environments & API Keys tab in the left menu.
You can copy your development API Key from the field at the top of this page.
(Your development key will start with `tr_dev_`).
## Adding Environment Variables
Create a `.env.local` file at the root of your project and include your Trigger API key and URL like this:
```bash
TRIGGER_API_KEY=ENTER_YOUR_DEVELOPMENT_API_KEY_HERE
TRIGGER_API_URL=https://cloud.trigger.dev
```
Replace `ENTER_YOUR_DEVELOPMENT_API_KEY_HERE` with the actual API key obtained from the previous step.
## Configuring the Trigger Client
To set up the Trigger Client for your project, follow these steps:
1. **Create Configuration File:**
In your project directory, create a configuration file named `trigger.ts` or `trigger.js`, depending on whether your project uses TypeScript (`.ts`) or JavaScript (`.js`).
2. **Choose Directory:**
Depending on your project structure, choose the appropriate directory for the configuration file. If your project uses a `src` directory, create the file within it. Otherwise, create it directly in the project root.
3. **Add Configuration Code:**
Open the configuration file you created and add the following code:
```typescript
// trigger.ts (for TypeScript) or trigger.js (for JavaScript)
import { TriggerClient } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "my-app",
apiKey: process.env.TRIGGER_API_KEY,
apiUrl: process.env.TRIGGER_API_URL,
});
```
Replace **"my-app"** with an appropriate identifier for your project. The **apiKey** and **apiUrl** are obtained from the environment variables you set earlier.
4. **File Location:**
Depending on your project structure, save the configuration file in the appropriate location:
- If your project uses a **src** directory, save the file within the **src** directory.
- If your project does not use a **src** directory, save the file in the project root.
**Example Directory Structure with src:**
```
project-root/
├── src/
├── trigger.ts
├── other files...
```
**Example Directory Structure without src:**
```
project-root/
├── trigger.ts
├── other files...
```
By following these steps, you'll configure the Trigger Client to work with your project, regardless of whether you have a separate **src** directory and whether you're using TypeScript or JavaScript files.
## Creating the API Route
To establish an API route for interacting with Trigger.dev, follow these steps based on your project's file type and structure
<Tabs>
<Tab title="App Directory">
1. Create a new file named `route.(ts/js)` within the `app/api/trigger/` directory.
2. Add the following code to `route.(ts/js)`:
```typescript
import { createAppRoute } from "@trigger.dev/nextjs";
import { client } from "@/trigger";
import "@/Jobs";
export const { POST, dynamic } = createAppRoute(client);
```
</Tab>
<Tab title="Pages Directory">
1. Create a new file named `trigger.(ts/js)` within the `pages/api/` directory.
2. Add the following code to `trigger.(ts/js)`:
```
import { createPagesRoute } from "@trigger.dev/nextjs";
import { client } from "@/trigger";
import "@/Jobs";
const { handler, config } = createPagesRoute(client);
export { config };
export default handler;
```
</Tab>
</Tabs>
<Warning>In the code blocks, replace "@/trigger" with the appropriate path to your Trigger Client configuration file, and adjust the path to the Jobs folder accordingly. Make sure to provide the correct paths if your project isn't utilizing the Next.js alias feature.</Warning>
## Creating the Example Job
1. Create a folder named `Jobs` alongside your `app` or `pages` directory
2. Inside the `Jobs` folder, add two files named `example.(ts/js)` and `index.(ts/js)`.
<CodeGroup>
```typescript example.(ts/js)
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "@/trigger";
// your first job
client.defineJob({
id: "example-job",
name: "Example Job",
version: "0.0.1",
trigger: eventTrigger({
name: "example.event",
}),
run: async (payload, io, ctx) => {
await io.logger.info("Hello world!", { payload });
return {
message: "Hello world!",
};
},
});
```
```typescript index.ts/index.(ts/js)
// import all your job files here
export * from "./examples"
```
</CodeGroup>
## Additonal Job Definations
You can define more job definitions by creating additional files in the `Jobs` folder and exporting them in `index` file.
For example, in `index.(ts/js)`, you can export other job files like this:
```typescript
// import all your job files here
export * from "./examples"
export * from "./other-job-file";
```
## Adding Configuration to `package.json`
Inside the `package.json` file, add the following configuration under the root object:
```json
"trigger.dev": {
"endpointId": "my-app"
}
```
Your `package.json` file might look something like this:
```json
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
// ... other dependencies
},
"trigger.dev": {
"endpointId": "my-app"
}
}
```
Replace **"my-app"** with the appropriate identifier you used during the step for creating the Trigger Client.
## Next Steps
Start your Next.js project locally locally, and then execute the `dev` CLI command to run Trigger.dev locally. You should run this command every time you want to use Trigger.dev locally.
![Your first Job](/images/cli-dev.gif)
<Warning>
Make sure your Next.js site is running locally before continuing. You must
also leave this `dev` terminal command running while you develop.
</Warning>
In a **new terminal window or tab** run:
<CodeGroup>
```bash npm
npx @trigger.dev/cli@latest dev
```
```bash pnpm
pnpm dlx @trigger.dev/cli@latest dev
```
```bash yarn
yarn dlx @trigger.dev/cli@latest dev
```
</CodeGroup>
<br />
<Note>
You can optionally pass the port if you're not running on 3000 by adding
`--port 3001` to the end
</Note>
<Tip>If your existing Next.js project utilizes middleware and you encounter any issues, such as potential conflicts with Trigger.dev, it's recommended to refer to the troubleshooting guide at [Middleware](/documentation/guides/platforms/nextjs#middleware) for assistance. This guide can help you address any concerns related to middleware conflicts and ensure the smooth functioning of your project with Trigger.dev.</Tip>
@@ -57,7 +57,7 @@ First, cd into your Next.js project, then run the `@trigger.dev/cli init` comman
npx @trigger.dev/cli@latest init -t "https://<your render app name>.onrender.com"
```
When it asks for your development API key, head over to your self-hosted Trigger.dev dashboard and select the initial project you created when signing up, and head to the `Environments & API Keys` page to copy your `dev` API key:
When it asks for your development API key, head over to your self-hosted Trigger.dev dashboard and select the initial project you created when signing up, and head to the `Environments & API Keys` page to copy your `dev` **SERVER** API key:
![api key](/images/api-key.png)
+1 -1
View File
@@ -35,7 +35,7 @@ Once you've created an account, follow the steps to:
1. Go to the "Environments & API Keys" page in your project.
![Go to the Environments & API Keys page ](/images/environments-link.png)
2. Copy the `DEV` API key.
2. Copy the `DEV` **SERVER** API key.
![API Keys](/images/api-keys.png)
## Run the CLI `init` command
+7 -4
View File
@@ -4,21 +4,24 @@ description: "An ever-growing list of example Jobs which you can use to get star
---
<Info>
If you are using integrations, you'll need set up authentication either using
OAuth or API keys / access tokens. You can find out how to do that in the
[integrations section](/integrations).
If you are using integrations, you'll need set up authentication either using OAuth or API keys /
access tokens. You can find out how to do that in the [integrations section](/integrations).
</Info>
Click the links below to view the Job code. You can also easily test these Jobs by following the instructions in the README of each example.
| Job (code in link) | Description | Integrations used |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [Basic delay](https://github.com/triggerdotdev/examples/blob/main/delays/src/jobs/delayJob.ts) | Logs a message to the console, waits for 5 minutes, and then logs another message. | N/A |
| [Basic interval](https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/interval.ts) | This Job will run every 60 seconds, starting 60 seconds after this Job is first indexed. | N/A |
| [Cron scheduled interval](https://github.com/triggerdotdev/examples/blob/main/scheduled/src/jobs/cronScheduled.ts) | A scheduled Job which runs at 2:30pm every Monday. | N/A |
| [OpenAI text summarizer](https://github.com/triggerdotdev/examples/blob/main/openai-text-summarizer/src/jobs/textSummarizer.ts) | Summarizes a block of text, pulling out the most unique and helpful points using OpenAI GPT-3.5 turbo. | [OpenAI](/integrations/apis/openai) |
| [Tell me a joke using OpenAI](https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/tellMeAJoke.ts) | Generates a random joke using OpenAI GPT 3.5. | [OpenAI](/integrations/apis/openai) |
| [Generate an image using OpenAI](https://github.com/triggerdotdev/examples/blob/main/openai/src/jobs/generateHedgehogImages.ts) | Generates a random image of a hedgehog using OpenAI DALL-E. | [OpenAI](/integrations/apis/openai) |
| [GitHub issue reminder](https://github.com/triggerdotdev/examples/blob/main/github-issue-reminder/jobs/githubIssue.ts) | Sends a Slack message to a channel if a GitHub issue is left open for 24 hours | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Github new star alert in Slack](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts) | When a repo is starred, a message is sent to a Slack channel with the name and URL of the GitHub user who starred the repo, and the updated Stargazers count. | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Add a custom label to a GitHub issue when it is created](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/onIssueOpened.ts) | When a new GitHub issue is opened it adds a "Bug" label to it. | [GitHub](/integrations/apis/github) |
| [GitHub new star alert](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarAlert.ts) | When a repo is starred a message is logged with the new Stargazers count. | [GitHub](/integrations/apis/github) |
| [Github new star alert in Slack](https://github.com/triggerdotdev/examples/blob/main/github/src/jobs/newStarToSlack.ts) | When a repo is starred, a message is sent to a Slack channel with the name and URL of the GitHub user who starred the repo, and the updated Stargazers count. | [GitHub](/integrations/apis/github), [Slack](/integrations/apis/slack) |
| [Send a Slack message when an event is received](https://github.com/triggerdotdev/examples/blob/main/slack/src/jobs/sendSlackMessage.ts) | Sends a Slack message to a specific channel when an event is received. | [Slack](/integrations/apis/slack) |
| [Send an email using Resend](https://github.com/triggerdotdev/examples/blob/main/resend/src/jobs/resendBasicEmail.ts) | Send a basic email using Resend | [Resend](/integrations/apis/resend) |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 69 KiB

+2
View File
@@ -83,6 +83,8 @@ Tasks that are marked as "long-running" can last longer than your serverless tim
| `listModels` | Lists the available models. |
| `createEdit` | Edits a given text prompt. |
| `createImage` | Generates images from textual descriptions. |
| `createImageEdit` | Creates an edited or extended image given an original image and a prompt |
| `createImageVariation` | Creates a variation of a given image. |
| `createEmbedding` | Generates embeddings for a given text. |
| `createFile` | Uploads a file to the OpenAI API. |
| `listFiles` | Lists the uploaded files. |
+15 -23
View File
@@ -7,8 +7,8 @@ description: "Interact with your Supabase project using the Supabase JS Client."
Our `@trigger.dev/supabase` package provides an integration that wraps the [@supabase/supabase-js](https://github.com/supabase/supabase-js) package, allowing you to run tasks to interact with your Supabase project.
<Note>
If you want to trigger jobs based on changes in your Supabase database, you'll
need to use the [Supabase Management API](../management) integration
If you want to trigger jobs based on changes in your Supabase database, you'll need to use the
[Supabase Management API](/integrations/apis/supabase/management) integration
</Note>
## Usage
@@ -26,8 +26,7 @@ const supabase = new Supabase({
```
<Warning>
Never expose the `service_role` key in a browser or anywhere where a user can
see it.
Never expose the `service_role` key in a browser or anywhere where a user can see it.
</Warning>
You can then use the `supabase` integration to run tasks in your jobs:
@@ -39,21 +38,17 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const { data: users, error } = await io.supabase.runTask(
"find-users",
async (db) => {
return db.from("users").select("*");
}
);
const { data: todos, error } = await io.supabase.runTask("find-todos", async (db) => {
return db.from("todos").select("*");
});
},
});
```
<Note>
By using `runTask` instead of the `@supabase/supabase-js` client directly
inside your job run, you'll be able to create tasks that can be run
idempotently and also retried. For more, see our guide on
[Resumability](http://localhost:3050/documentation/concepts/resumability)
By using `runTask` instead of the `@supabase/supabase-js` client directly inside your job run,
you'll be able to create tasks that can be run idempotently and also retried. For more, see our
guide on [Resumability](http://localhost:3050/documentation/concepts/resumability)
</Note>
You can also choose to throw an error if the query fails and abort the job run:
@@ -65,8 +60,8 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const users = await io.supabase.runTask("find-users", async (db) => {
const { data, error } = await db.from("users").select("*");
const todos = await io.supabase.runTask("find-todos", async (db) => {
const { data, error } = await db.from("todos").select("*");
if (error) throw error;
@@ -83,10 +78,7 @@ The `db` object passed to the callback is an instance of the [@supabase/supabase
- [Invoking Functions](https://supabase.com/docs/reference/javascript/functions-invoke)
- [Storage](https://supabase.com/docs/reference/javascript/storage-createbucket)
<Warning>
Currently we do not support Supabase Realtime (such as subscribing to a
channel)
</Warning>
<Warning>Currently we do not support Supabase Realtime (such as subscribing to a channel)</Warning>
## Typescript Support
@@ -108,15 +100,15 @@ client.defineJob({
supabase,
},
run: async (payload, io, ctx) => {
const users = await io.supabase.runTask("find-users", async (db) => {
const { data, error } = await db.from("users").select("*");
const todos = await io.supabase.runTask("find-todos", async (db) => {
const { data, error } = await db.from("todos").select("*");
if (error) throw error;
return data;
});
// users is now typed as User[] instead of any[]
// todos is now typed as Todo[] instead of any[]
},
});
```
+28 -17
View File
@@ -126,7 +126,7 @@ client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onInserted({
table: "users",
table: "todos",
}),
run: async (payload, io, ctx) => {
// payload is the database webhook body (see https://supabase.com/docs/guides/database/webhooks#payload)
@@ -137,20 +137,6 @@ client.defineJob({
You can add additional filters to the trigger by passing a `filter` object:
```ts
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onUpdated({
table: "users",
filter: {
country: ["USA", "Canada"], // This will only trigger the job if the user.country is USA or Canada
},
}),
run: async (payload, io, ctx) => {
// payload is the database webhook body (see https://supabase.com/docs/guides/database/webhooks#payload)
},
});
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
@@ -172,6 +158,31 @@ client.defineJob({
});
```
You can also listen for multiple different events using the `on` trigger:
```ts
client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.on({
table: "todos",
events: ["INSERT", "UPDATE"] // Trigger on both insert and update events
filter: {
record: {
is_completed: [false],
},
},
}),
run: async (payload, io, ctx) => {
if (payload.type === "INSERT") {
// payload will be typed as the INSERT payload
} else {
// payload will be typed as the UPDATE payload
}
},
});
```
<Note>
We will only create at most 1 database webhook per table, to limit resource usage when writing to
your database. This means we cannot support scoping updated triggers to specific columns.
@@ -196,10 +207,10 @@ client.defineJob({
id: "supabase-trigger",
name: "Supabase Trigger",
trigger: db.onUpdated({
table: "users",
table: "todos",
}),
run: async (payload, io, ctx) => {
// payload.record and payload.old_record are now correctly typed to match the users table
// payload.record and payload.old_record are now correctly typed to match the todos table
},
});
```
+11 -27
View File
@@ -23,7 +23,8 @@
},
"feedback": {
"suggestEdit": true,
"raiseIssue": true
"raiseIssue": true,
"thumbsRating": true
},
"topbarCtaButton": {
"type": "github",
@@ -102,6 +103,7 @@
]
},
"documentation/guides/cli",
"documentation/guides/manual",
"documentation/guides/running-jobs",
{
"group": "Using the Dashboard",
@@ -154,10 +156,7 @@
},
{
"group": "Overview",
"pages": [
"integrations/introduction",
"integrations/create"
]
"pages": ["integrations/introduction", "integrations/create"]
},
{
"group": "Integrations",
@@ -180,22 +179,16 @@
},
{
"group": "OpenAI",
"pages": [
"integrations/apis/openai"
]
"pages": ["integrations/apis/openai"]
},
"integrations/apis/plain",
{
"group": "Resend",
"pages": [
"integrations/apis/resend"
]
"pages": ["integrations/apis/resend"]
},
{
"group": "Slack",
"pages": [
"integrations/apis/slack"
]
"pages": ["integrations/apis/slack"]
},
"integrations/apis/typeform"
]
@@ -249,10 +242,7 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
}
]
},
@@ -263,10 +253,7 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
}
]
},
@@ -287,10 +274,7 @@
},
{
"group": "Overview",
"pages": [
"examples/introduction",
"examples/examples-repository"
]
"pages": ["examples/introduction", "examples/examples-repository"]
}
],
"footerSocials": {
@@ -303,4 +287,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
+11 -1
View File
@@ -2,8 +2,18 @@
This project is meant to be used to create a catalog of jobs, usually to test something in an integration or the SDK.
## Setup
You will need to create a `.env` file. You can duplicate the `.env.example` file and set your local `TRIGGER_API_KEY` value.
### Running
You need to build the CLI:
```sh
pnpm run build --filter @trigger.dev/cli
```
Each file in `src` is a separate set of jobs that can be run separately. For example, the `src/stripe.ts` file can be run with:
```sh
@@ -15,7 +25,7 @@ This will open up a local server using `express` on port 8080. Then in a new ter
```sh
cd examples/job-catalog
pnpm run trigger:dev
pnpm run dev:trigger
```
### Adding a new file
+1
View File
@@ -5,6 +5,7 @@
"scripts": {
"stripe": "nodemon --watch src/stripe.ts -r tsconfig-paths/register -r dotenv/config src/stripe.ts",
"supabase": "nodemon --watch src/supabase.ts -r tsconfig-paths/register -r dotenv/config src/supabase.ts",
"supabase:types": "npx supabase gen types typescript --project-id $SUPABASE_PROJECT_ID --schema public --schema auth --schema storage > src/supabase-types.ts",
"dev:trigger": "trigger-cli dev --port 8080"
},
"dependencies": {
+843
View File
@@ -0,0 +1,843 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
auth: {
Tables: {
audit_log_entries: {
Row: {
created_at: string | null
id: string
instance_id: string | null
ip_address: string
payload: Json | null
}
Insert: {
created_at?: string | null
id: string
instance_id?: string | null
ip_address?: string
payload?: Json | null
}
Update: {
created_at?: string | null
id?: string
instance_id?: string | null
ip_address?: string
payload?: Json | null
}
Relationships: []
}
flow_state: {
Row: {
auth_code: string
authentication_method: string
code_challenge: string
code_challenge_method: Database["auth"]["Enums"]["code_challenge_method"]
created_at: string | null
id: string
provider_access_token: string | null
provider_refresh_token: string | null
provider_type: string
updated_at: string | null
user_id: string | null
}
Insert: {
auth_code: string
authentication_method: string
code_challenge: string
code_challenge_method: Database["auth"]["Enums"]["code_challenge_method"]
created_at?: string | null
id: string
provider_access_token?: string | null
provider_refresh_token?: string | null
provider_type: string
updated_at?: string | null
user_id?: string | null
}
Update: {
auth_code?: string
authentication_method?: string
code_challenge?: string
code_challenge_method?: Database["auth"]["Enums"]["code_challenge_method"]
created_at?: string | null
id?: string
provider_access_token?: string | null
provider_refresh_token?: string | null
provider_type?: string
updated_at?: string | null
user_id?: string | null
}
Relationships: []
}
identities: {
Row: {
created_at: string | null
email: string | null
id: string
identity_data: Json
last_sign_in_at: string | null
provider: string
updated_at: string | null
user_id: string
}
Insert: {
created_at?: string | null
email?: string | null
id: string
identity_data: Json
last_sign_in_at?: string | null
provider: string
updated_at?: string | null
user_id: string
}
Update: {
created_at?: string | null
email?: string | null
id?: string
identity_data?: Json
last_sign_in_at?: string | null
provider?: string
updated_at?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "identities_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
instances: {
Row: {
created_at: string | null
id: string
raw_base_config: string | null
updated_at: string | null
uuid: string | null
}
Insert: {
created_at?: string | null
id: string
raw_base_config?: string | null
updated_at?: string | null
uuid?: string | null
}
Update: {
created_at?: string | null
id?: string
raw_base_config?: string | null
updated_at?: string | null
uuid?: string | null
}
Relationships: []
}
mfa_amr_claims: {
Row: {
authentication_method: string
created_at: string
id: string
session_id: string
updated_at: string
}
Insert: {
authentication_method: string
created_at: string
id: string
session_id: string
updated_at: string
}
Update: {
authentication_method?: string
created_at?: string
id?: string
session_id?: string
updated_at?: string
}
Relationships: [
{
foreignKeyName: "mfa_amr_claims_session_id_fkey"
columns: ["session_id"]
referencedRelation: "sessions"
referencedColumns: ["id"]
}
]
}
mfa_challenges: {
Row: {
created_at: string
factor_id: string
id: string
ip_address: unknown
verified_at: string | null
}
Insert: {
created_at: string
factor_id: string
id: string
ip_address: unknown
verified_at?: string | null
}
Update: {
created_at?: string
factor_id?: string
id?: string
ip_address?: unknown
verified_at?: string | null
}
Relationships: [
{
foreignKeyName: "mfa_challenges_auth_factor_id_fkey"
columns: ["factor_id"]
referencedRelation: "mfa_factors"
referencedColumns: ["id"]
}
]
}
mfa_factors: {
Row: {
created_at: string
factor_type: Database["auth"]["Enums"]["factor_type"]
friendly_name: string | null
id: string
secret: string | null
status: Database["auth"]["Enums"]["factor_status"]
updated_at: string
user_id: string
}
Insert: {
created_at: string
factor_type: Database["auth"]["Enums"]["factor_type"]
friendly_name?: string | null
id: string
secret?: string | null
status: Database["auth"]["Enums"]["factor_status"]
updated_at: string
user_id: string
}
Update: {
created_at?: string
factor_type?: Database["auth"]["Enums"]["factor_type"]
friendly_name?: string | null
id?: string
secret?: string | null
status?: Database["auth"]["Enums"]["factor_status"]
updated_at?: string
user_id?: string
}
Relationships: [
{
foreignKeyName: "mfa_factors_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
refresh_tokens: {
Row: {
created_at: string | null
id: number
instance_id: string | null
parent: string | null
revoked: boolean | null
session_id: string | null
token: string | null
updated_at: string | null
user_id: string | null
}
Insert: {
created_at?: string | null
id?: number
instance_id?: string | null
parent?: string | null
revoked?: boolean | null
session_id?: string | null
token?: string | null
updated_at?: string | null
user_id?: string | null
}
Update: {
created_at?: string | null
id?: number
instance_id?: string | null
parent?: string | null
revoked?: boolean | null
session_id?: string | null
token?: string | null
updated_at?: string | null
user_id?: string | null
}
Relationships: [
{
foreignKeyName: "refresh_tokens_session_id_fkey"
columns: ["session_id"]
referencedRelation: "sessions"
referencedColumns: ["id"]
}
]
}
saml_providers: {
Row: {
attribute_mapping: Json | null
created_at: string | null
entity_id: string
id: string
metadata_url: string | null
metadata_xml: string
sso_provider_id: string
updated_at: string | null
}
Insert: {
attribute_mapping?: Json | null
created_at?: string | null
entity_id: string
id: string
metadata_url?: string | null
metadata_xml: string
sso_provider_id: string
updated_at?: string | null
}
Update: {
attribute_mapping?: Json | null
created_at?: string | null
entity_id?: string
id?: string
metadata_url?: string | null
metadata_xml?: string
sso_provider_id?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "saml_providers_sso_provider_id_fkey"
columns: ["sso_provider_id"]
referencedRelation: "sso_providers"
referencedColumns: ["id"]
}
]
}
saml_relay_states: {
Row: {
created_at: string | null
for_email: string | null
from_ip_address: unknown | null
id: string
redirect_to: string | null
request_id: string
sso_provider_id: string
updated_at: string | null
}
Insert: {
created_at?: string | null
for_email?: string | null
from_ip_address?: unknown | null
id: string
redirect_to?: string | null
request_id: string
sso_provider_id: string
updated_at?: string | null
}
Update: {
created_at?: string | null
for_email?: string | null
from_ip_address?: unknown | null
id?: string
redirect_to?: string | null
request_id?: string
sso_provider_id?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "saml_relay_states_sso_provider_id_fkey"
columns: ["sso_provider_id"]
referencedRelation: "sso_providers"
referencedColumns: ["id"]
}
]
}
schema_migrations: {
Row: {
version: string
}
Insert: {
version: string
}
Update: {
version?: string
}
Relationships: []
}
sessions: {
Row: {
aal: Database["auth"]["Enums"]["aal_level"] | null
created_at: string | null
factor_id: string | null
id: string
not_after: string | null
updated_at: string | null
user_id: string
}
Insert: {
aal?: Database["auth"]["Enums"]["aal_level"] | null
created_at?: string | null
factor_id?: string | null
id: string
not_after?: string | null
updated_at?: string | null
user_id: string
}
Update: {
aal?: Database["auth"]["Enums"]["aal_level"] | null
created_at?: string | null
factor_id?: string | null
id?: string
not_after?: string | null
updated_at?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "sessions_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
sso_domains: {
Row: {
created_at: string | null
domain: string
id: string
sso_provider_id: string
updated_at: string | null
}
Insert: {
created_at?: string | null
domain: string
id: string
sso_provider_id: string
updated_at?: string | null
}
Update: {
created_at?: string | null
domain?: string
id?: string
sso_provider_id?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "sso_domains_sso_provider_id_fkey"
columns: ["sso_provider_id"]
referencedRelation: "sso_providers"
referencedColumns: ["id"]
}
]
}
sso_providers: {
Row: {
created_at: string | null
id: string
resource_id: string | null
updated_at: string | null
}
Insert: {
created_at?: string | null
id: string
resource_id?: string | null
updated_at?: string | null
}
Update: {
created_at?: string | null
id?: string
resource_id?: string | null
updated_at?: string | null
}
Relationships: []
}
users: {
Row: {
aud: string | null
banned_until: string | null
confirmation_sent_at: string | null
confirmation_token: string | null
confirmed_at: string | null
created_at: string | null
deleted_at: string | null
email: string | null
email_change: string | null
email_change_confirm_status: number | null
email_change_sent_at: string | null
email_change_token_current: string | null
email_change_token_new: string | null
email_confirmed_at: string | null
encrypted_password: string | null
id: string
instance_id: string | null
invited_at: string | null
is_sso_user: boolean
is_super_admin: boolean | null
last_sign_in_at: string | null
phone: string | null
phone_change: string | null
phone_change_sent_at: string | null
phone_change_token: string | null
phone_confirmed_at: string | null
raw_app_meta_data: Json | null
raw_user_meta_data: Json | null
reauthentication_sent_at: string | null
reauthentication_token: string | null
recovery_sent_at: string | null
recovery_token: string | null
role: string | null
updated_at: string | null
}
Insert: {
aud?: string | null
banned_until?: string | null
confirmation_sent_at?: string | null
confirmation_token?: string | null
confirmed_at?: string | null
created_at?: string | null
deleted_at?: string | null
email?: string | null
email_change?: string | null
email_change_confirm_status?: number | null
email_change_sent_at?: string | null
email_change_token_current?: string | null
email_change_token_new?: string | null
email_confirmed_at?: string | null
encrypted_password?: string | null
id: string
instance_id?: string | null
invited_at?: string | null
is_sso_user?: boolean
is_super_admin?: boolean | null
last_sign_in_at?: string | null
phone?: string | null
phone_change?: string | null
phone_change_sent_at?: string | null
phone_change_token?: string | null
phone_confirmed_at?: string | null
raw_app_meta_data?: Json | null
raw_user_meta_data?: Json | null
reauthentication_sent_at?: string | null
reauthentication_token?: string | null
recovery_sent_at?: string | null
recovery_token?: string | null
role?: string | null
updated_at?: string | null
}
Update: {
aud?: string | null
banned_until?: string | null
confirmation_sent_at?: string | null
confirmation_token?: string | null
confirmed_at?: string | null
created_at?: string | null
deleted_at?: string | null
email?: string | null
email_change?: string | null
email_change_confirm_status?: number | null
email_change_sent_at?: string | null
email_change_token_current?: string | null
email_change_token_new?: string | null
email_confirmed_at?: string | null
encrypted_password?: string | null
id?: string
instance_id?: string | null
invited_at?: string | null
is_sso_user?: boolean
is_super_admin?: boolean | null
last_sign_in_at?: string | null
phone?: string | null
phone_change?: string | null
phone_change_sent_at?: string | null
phone_change_token?: string | null
phone_confirmed_at?: string | null
raw_app_meta_data?: Json | null
raw_user_meta_data?: Json | null
reauthentication_sent_at?: string | null
reauthentication_token?: string | null
recovery_sent_at?: string | null
recovery_token?: string | null
role?: string | null
updated_at?: string | null
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
email: {
Args: Record<PropertyKey, never>
Returns: string
}
jwt: {
Args: Record<PropertyKey, never>
Returns: Json
}
role: {
Args: Record<PropertyKey, never>
Returns: string
}
uid: {
Args: Record<PropertyKey, never>
Returns: string
}
}
Enums: {
aal_level: "aal1" | "aal2" | "aal3"
code_challenge_method: "s256" | "plain"
factor_status: "unverified" | "verified"
factor_type: "totp" | "webauthn"
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
todos: {
Row: {
id: number
inserted_at: string
is_complete: boolean | null
task: string | null
user_id: string
}
Insert: {
id?: number
inserted_at?: string
is_complete?: boolean | null
task?: string | null
user_id: string
}
Update: {
id?: number
inserted_at?: string
is_complete?: boolean | null
task?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "todos_user_id_fkey"
columns: ["user_id"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null
avif_autodetection: boolean | null
created_at: string | null
file_size_limit: number | null
id: string
name: string
owner: string | null
public: boolean | null
updated_at: string | null
}
Insert: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id: string
name: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
Update: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id?: string
name?: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "buckets_owner_fkey"
columns: ["owner"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
migrations: {
Row: {
executed_at: string | null
hash: string
id: number
name: string
}
Insert: {
executed_at?: string | null
hash: string
id: number
name: string
}
Update: {
executed_at?: string | null
hash?: string
id?: number
name?: string
}
Relationships: []
}
objects: {
Row: {
bucket_id: string | null
created_at: string | null
id: string
last_accessed_at: string | null
metadata: Json | null
name: string | null
owner: string | null
path_tokens: string[] | null
updated_at: string | null
version: string | null
}
Insert: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Update: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Relationships: [
{
foreignKeyName: "objects_bucketId_fkey"
columns: ["bucket_id"]
referencedRelation: "buckets"
referencedColumns: ["id"]
},
{
foreignKeyName: "objects_owner_fkey"
columns: ["owner"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
}
Views: {
[_ in never]: never
}
Functions: {
can_insert_object: {
Args: {
bucketid: string
name: string
owner: string
metadata: Json
}
Returns: undefined
}
extension: {
Args: {
name: string
}
Returns: string
}
filename: {
Args: {
name: string
}
Returns: string
}
foldername: {
Args: {
name: string
}
Returns: unknown
}
get_size_by_bucket: {
Args: Record<PropertyKey, never>
Returns: {
size: number
bucket_id: string
}[]
}
search: {
Args: {
prefix: string
bucketname: string
limits?: number
levels?: number
offsets?: number
search?: string
sortcolumn?: string
sortorder?: string
}
Returns: {
name: string
id: string
updated_at: string
created_at: string
last_accessed_at: string
metadata: Json
}[]
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
+122 -8
View File
@@ -1,14 +1,26 @@
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { TriggerClient } from "@trigger.dev/sdk";
import { createExpressServer } from "@trigger.dev/express";
import { z } from "zod";
import { SupabaseManagement } from "@trigger.dev/supabase";
import { Supabase, SupabaseManagement } from "@trigger.dev/supabase";
import { OpenAI } from "@trigger.dev/openai";
import { Database } from "./supabase-types";
const supabaseManagement = new SupabaseManagement({
id: "supabase-management",
apiKey: process.env["SUPABASE_API_KEY"]!,
});
const db = supabaseManagement.db(process.env["SUPABASE_ID"]!);
const triggers = supabaseManagement.db<Database>(process.env["SUPABASE_ID"]!);
const supabase = new Supabase({
id: "supabase",
supabaseKey: process.env["SUPABASE_SERVICE_ROLE_KEY"]!,
supabaseUrl: process.env["SUPABASE_URL"]!,
});
const openai = new OpenAI({
id: "open-ai",
apiKey: process.env["OPENAI_API_KEY"]!,
});
export const client = new TriggerClient({
id: "job-catalog",
@@ -24,8 +36,8 @@ client.defineJob({
id: "supabase-management-example-1",
name: "Supabase Management Example 1",
version: "0.1.0",
trigger: db.onInserted({
table: "users",
trigger: triggers.onInserted({
table: "todos",
}),
run: async (payload, io, ctx) => {},
});
@@ -34,8 +46,110 @@ client.defineJob({
id: "supabase-management-example-2",
name: "Supabase Management Example 2",
version: "0.1.0",
trigger: db.onUpdated({
table: "users",
trigger: triggers.onUpdated({
table: "todos",
}),
run: async (payload, io, ctx) => {},
});
client.defineJob({
id: "supabase-management-example-users-auth",
name: "Supabase Management Example Users Auth",
version: "0.1.0",
trigger: triggers.onInserted({
table: "users",
schema: "auth",
}),
run: async (payload, io, ctx) => {},
});
client.defineJob({
id: "supabase-management-example-objects-storage",
name: "Supabase Management Example Object Storage",
version: "0.1.0",
trigger: triggers.onInserted({
schema: "storage",
table: "objects",
filter: {
record: {
bucket_id: ["example_bucket"],
name: [
{
$endsWith: ".png",
},
],
path_tokens: [
{
$includes: "images",
},
],
},
},
}),
integrations: {
openai,
supabase,
},
run: async (payload, io, ctx) => {
if (!payload.record.name) {
return;
}
const {
data: { publicUrl },
} = supabase.native.storage.from("example_bucket").getPublicUrl(payload.record.name);
// Use the native supabase client to get a signed url
const { error, data } = await io.supabase.client.storage
.from("example_bucket")
.createSignedUrl(payload.record.name, 60);
if (error) {
throw error;
}
const imageVariation = await io.openai.createImageVariation("variation-image", {
image: data.signedUrl,
n: 2,
response_format: "url",
size: "512x512",
});
const imageEdit = await io.openai.createImageEdit("edit-image", {
image: publicUrl,
prompt:
"Fill in the background to make it seem like the cat is on the moon with a beautiful view of the earth.",
n: 2,
response_format: "url",
size: "512x512",
});
// return imageEdit;
},
});
client.defineJob({
id: "supabase-management-example-on",
name: "Supabase Management Example On",
version: "0.1.0",
trigger: triggers.on({
table: "todos",
events: ["INSERT", "UPDATE"],
}),
integrations: {
supabase,
},
run: async (payload, io, ctx) => {
const user = await io.supabase.runTask("fetch-user", async (db) => {
const { data, error } = await db.auth.admin.getUserById(payload.record.user_id);
if (error) {
throw error;
}
return data.user;
});
return user;
},
});
+3
View File
@@ -0,0 +1,3 @@
TRIGGER_API_KEY=tr_dev_test-api-key
TRIGGER_API_URL=http://localhost:3030
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
+35
View File
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+34
View File
@@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
+4
View File
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@examples/nextjs-test",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently --kill-others \"pnpm dev:next\" \"wait-on http://localhost:3000 && pnpm dev:trigger\"",
"dev:next": "next dev",
"dev:trigger": "trigger-cli dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "13.4.12",
"react": "18.2.0",
"react-dom": "18.2.0",
"@trigger.dev/sdk": "^2.0.0-next.22",
"@trigger.dev/nextjs": "^1.0.0-next.8"
},
"trigger.dev": {
"endpointId": "nextjs-test"
},
"devDependencies": {
"@trigger.dev/cli": "workspace:*",
"concurrently": "^8.2.0",
"eslint": "8.42.0",
"eslint-config-next": "13.4.6",
"wait-on": "^7.0.1"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

@@ -0,0 +1,9 @@
import { createAppRoute } from "@trigger.dev/nextjs";
import { client } from "@/trigger";
// Replace this with your own jobs
import "@/jobs/examples";
//this route is used to send and receive data with Trigger.dev
export const { POST, dynamic } = createAppRoute(client);
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+107
View File
@@ -0,0 +1,107 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
+17
View File
@@ -0,0 +1,17 @@
import './globals.css'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
+95
View File
@@ -0,0 +1,95 @@
import Image from 'next/image'
import styles from './page.module.css'
export default function Home() {
return (
<main className={styles.main}>
<div className={styles.description}>
<p>
Get started by editing&nbsp;
<code className={styles.code}>src/app/page.js</code>
</p>
<div>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className={styles.vercelLogo}
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className={styles.center}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className={styles.grid}>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Docs <span>-&gt;</span>
</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Learn <span>-&gt;</span>
</h2>
<p>Learn about Next.js in an interactive course with&nbsp;quizzes!</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Templates <span>-&gt;</span>
</h2>
<p>Explore the Next.js 13 playground.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Deploy <span>-&gt;</span>
</h2>
<p>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
}
@@ -0,0 +1,229 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
width: var(--max-width);
max-width: 100%;
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo {
position: relative;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
+20
View File
@@ -0,0 +1,20 @@
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "@/trigger";
// your first job
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!",
};
},
});
+9
View File
@@ -0,0 +1,9 @@
import { TriggerClient } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "nextjs-test",
apiKey: process.env.TRIGGER_API_KEY,
apiUrl: process.env.TRIGGER_API_URL,
});
+30
View File
@@ -1,5 +1,35 @@
# @trigger.dev/github
## 2.0.7
### Patch Changes
- Updated dependencies [84126808]
- @trigger.dev/integration-kit@2.0.7
- @trigger.dev/sdk@2.0.7
## 2.0.6
### Patch Changes
- @trigger.dev/integration-kit@2.0.6
- @trigger.dev/sdk@2.0.6
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "2.0.3",
"version": "2.0.7",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -29,8 +29,8 @@
"@octokit/request": "^6.2.5",
"@octokit/request-error": "^4.0.1",
"@octokit/webhooks": "^10.4.0",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.7",
"@trigger.dev/integration-kit": "workspace:^2.0.7",
"octokit": "^2.0.14",
"zod": "3.21.4"
},
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/slack
## 2.0.7
### Patch Changes
- 84126808: Adding createImageEdit and createImageVariation tasks to openai
- Updated dependencies [84126808]
- @trigger.dev/integration-kit@2.0.7
- @trigger.dev/sdk@2.0.7
## 2.0.6
### Patch Changes
- @trigger.dev/integration-kit@2.0.6
- @trigger.dev/sdk@2.0.6
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "2.0.3",
"version": "2.0.7",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
},
"dependencies": {
"openai": "^3.3.0",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.7",
"@trigger.dev/integration-kit": "workspace:^2.0.7",
"zod": "3.21.4"
},
"engines": {
+154 -4
View File
@@ -10,7 +10,7 @@ import {
} from "openai";
import { OpenAIIntegrationAuth } from "./types";
import { redactString } from "@trigger.dev/sdk";
import { Prettify, fileFromString, truncate } from "@trigger.dev/integration-kit";
import { Prettify, fileFromString, fileFromUrl, truncate } from "@trigger.dev/integration-kit";
import { createTaskUsageProperties, onTaskError } from "./taskUtils";
type OpenAIClientType = InstanceType<typeof OpenAIApi>;
@@ -326,6 +326,156 @@ export const createImage: AuthenticatedTask<
},
};
export type CreateImageEditRequest = {
image: string | File;
prompt: string;
mask?: string | File;
n?: number;
size?: "256x256" | "512x512" | "1024x1024";
response_format?: "url" | "b64_json";
user?: string;
};
type CreateImageEditResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createImageEdit"]>>["data"]
>;
export const createImageEdit: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateImageEditRequest>,
CreateImageEditResponseData
> = {
onError: onTaskError,
run: async (params, client, task) => {
const file = typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const mask = typeof params.mask === "string" ? await fileFromUrl(params.mask) : params.mask;
const response = await client.createImageEdit(
file,
params.prompt,
mask,
params.n,
params.size,
params.response_format,
params.user
);
return response.data;
},
init: (params) => {
let properties = [];
properties.push({
label: "Prompt",
text: params.prompt,
});
if (params.n) {
properties.push({
label: "Number of images",
text: params.n.toString(),
});
}
if (params.size) {
properties.push({
label: "Size",
text: params.size,
});
}
if (params.response_format) {
properties.push({
label: "Response format",
text: params.response_format,
});
}
if (typeof params.image === "string") {
properties.push({
label: "Image URL",
text: params.image,
url: params.image,
});
}
return {
name: "Create image edit",
params,
icon: "openai",
properties,
};
},
};
type CreateImageVariationResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createImageVariation"]>>["data"]
>;
export type CreateImageVariationRequest = {
image: string | File;
n?: number;
size?: "256x256" | "512x512" | "1024x1024";
response_format?: "url" | "b64_json";
user?: string;
};
export const createImageVariation: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateImageVariationRequest>,
CreateImageVariationResponseData
> = {
onError: onTaskError,
run: async (params, client, task) => {
const file = typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const response = await client
.createImageVariation(file, params.n, params.size, params.response_format, params.user)
.then((res) => res.data);
return response;
},
init: (params) => {
let properties = [];
if (params.n) {
properties.push({
label: "Number of images",
text: params.n.toString(),
});
}
if (params.size) {
properties.push({
label: "Size",
text: params.size,
});
}
if (params.response_format) {
properties.push({
label: "Response format",
text: params.response_format,
});
}
if (typeof params.image === "string") {
properties.push({
label: "Image URL",
text: params.image,
url: params.image,
});
}
return {
name: "Create image variation",
params,
icon: "openai",
properties,
};
},
};
type CreateEmbeddingResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createEmbedding"]>>["data"]
>;
@@ -383,7 +533,7 @@ export const createFile: AuthenticatedTask<
let file: File;
if (typeof params.file === "string") {
file = (await fileFromString(params.file, params.fileName ?? "file.txt")) as any;
file = await fileFromString(params.file, params.fileName ?? "file.txt");
} else {
file = params.file;
}
@@ -441,10 +591,10 @@ export const createFineTuneFile: AuthenticatedTask<
> = {
onError: onTaskError,
run: async (params, client) => {
const file = (await fileFromString(
const file = await fileFromString(
params.examples.map((d) => JSON.stringify(d)).join("\n"),
params.fileName
)) as any;
);
return client.createFile(file, "fine-tune").then((res) => res.data);
},
+30
View File
@@ -1,5 +1,35 @@
# @trigger.dev/plain
## 2.0.7
### Patch Changes
- Updated dependencies [84126808]
- @trigger.dev/integration-kit@2.0.7
- @trigger.dev/sdk@2.0.7
## 2.0.6
### Patch Changes
- @trigger.dev/integration-kit@2.0.6
- @trigger.dev/sdk@2.0.6
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "2.0.3",
"version": "2.0.7",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.7",
"@trigger.dev/sdk": "workspace:^2.0.7",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+30
View File
@@ -1,5 +1,35 @@
# @trigger.dev/resend
## 2.0.7
### Patch Changes
- Updated dependencies [84126808]
- @trigger.dev/integration-kit@2.0.7
- @trigger.dev/sdk@2.0.7
## 2.0.6
### Patch Changes
- @trigger.dev/integration-kit@2.0.6
- @trigger.dev/sdk@2.0.6
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "2.0.3",
"version": "2.0.7",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.7",
"@trigger.dev/sdk": "workspace:^2.0.7",
"resend": "^0.9.1"
},
"engines": {
+25
View File
@@ -1,5 +1,30 @@
# @trigger.dev/slack
## 2.0.7
### Patch Changes
- @trigger.dev/sdk@2.0.7
## 2.0.6
### Patch Changes
- @trigger.dev/sdk@2.0.6
## 2.0.5
### Patch Changes
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
## 2.0.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "2.0.3",
"version": "2.0.7",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.7",
"zod": "3.21.4"
},
"engines": {
+30
View File
@@ -1,5 +1,35 @@
# @trigger.dev/stripe
## 2.0.7
### Patch Changes
- Updated dependencies [84126808]
- @trigger.dev/integration-kit@2.0.7
- @trigger.dev/sdk@2.0.7
## 2.0.6
### Patch Changes
- @trigger.dev/integration-kit@2.0.6
- @trigger.dev/sdk@2.0.6
## 2.0.5
### Patch Changes
- @trigger.dev/integration-kit@2.0.5
- @trigger.dev/sdk@2.0.5
## 2.0.4
### Patch Changes
- Updated dependencies [96384991]
- @trigger.dev/sdk@2.0.4
- @trigger.dev/integration-kit@2.0.4
## 2.0.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "2.0.3",
"version": "2.0.7",
"description": "Trigger.dev integration for stripe",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.0.3",
"@trigger.dev/sdk": "workspace:^2.0.3",
"@trigger.dev/integration-kit": "workspace:^2.0.7",
"@trigger.dev/sdk": "workspace:^2.0.7",
"stripe": "^12.14.0",
"zod": "3.21.4"
},

Some files were not shown because too many files have changed in this diff Show More