Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7c1e98e5e | |||
| 71a0afeb8d | |||
| 36a390449a | |||
| 375e495423 | |||
| 866c20cd39 | |||
| 408785b8ef | |||
| f982e00997 | |||
| 7e100b8362 | |||
| ea052c44c7 | |||
| 76224568f4 | |||
| 72e67981b0 | |||
| cc44aff24b | |||
| 07d28ee4f5 | |||
| f16a2cc4e6 | |||
| b859837564 | |||
| 1343003efa | |||
| 0c4aea0338 | |||
| 627c767c64 | |||
| f3a0c3a5d1 | |||
| 7bca3d9c15 | |||
| 01fc1c6634 | |||
| 2d4f85e9f4 | |||
| 8718b28949 | |||
| 2caa2a5bbd | |||
| 43a10ee695 | |||
| 018005d478 | |||
| 4da187c4c5 | |||
| 4925424778 | |||
| 7ef61222ed | |||
| 044d38e390 | |||
| e7768881af | |||
| 5ea6a49d08 | |||
| dce5578327 | |||
| fde3f17a25 | |||
| a6c9cac26c | |||
| 960d99461a | |||
| e5307133b3 | |||
| 34bb3acc23 | |||
| 6d3b761cd7 | |||
| abc9737a4f | |||
| 84af64165c | |||
| 0adf41c7f0 | |||
| b7f4e40edb | |||
| cf33396dc0 | |||
| 51f2cd6b3f | |||
| 513d0f071d | |||
| 50e3192453 | |||
| 1524c95a7a | |||
| c8aaea8ad0 | |||
| cf7f25abcd | |||
| 99453e8581 | |||
| bc7206d981 | |||
| 64b8e150ea | |||
| b3b5852a88 | |||
| d1ecd6b99c | |||
| 0fb16a7324 | |||
| 4f317055fd | |||
| 3afd5f8cf3 | |||
| c922d24941 | |||
| 39ec34f84f | |||
| b9eba68078 | |||
| 9df93d0798 | |||
| 6769d6b439 | |||
| 70410d33af | |||
| 6371dd4f95 | |||
| ebe78017ef | |||
| b697a19954 | |||
| 29ca09e42d | |||
| 375b788ab0 | |||
| d6b44d13f7 | |||
| c6784ba7b8 | |||
| 6b8b1c7cae | |||
| 0c20756624 | |||
| 3d387d602d | |||
| 5d45dea0e1 | |||
| a25751e805 | |||
| 659be8cbc1 | |||
| 50e3d9e43a | |||
| 203a431350 | |||
| 39a332eff1 | |||
| 498f56de3e | |||
| b23696b255 | |||
| f3a7e2aa20 | |||
| b737f7af92 | |||
| f54a9ff5fc | |||
| 067c0db543 | |||
| 773765f82d | |||
| 624072c23b | |||
| d37576f814 | |||
| 476e2f035f | |||
| ad71964d30 | |||
| 975c5f1d3c | |||
| 916ed4e366 | |||
| df4667b7f5 | |||
| d663ba8985 | |||
| 8c8d1598e3 | |||
| 646bfd37bd | |||
| c0d5292b06 | |||
| 8c383bdc69 | |||
| 97e63d1222 | |||
| 87ee98aff1 | |||
| c070e78d06 | |||
| 1c4f9ff1ff | |||
| ca031a72e7 | |||
| cbc976188e | |||
| 59a94c710e | |||
| 4cd97c81ea | |||
| d0d5a698ec | |||
| ee033a1fd4 | |||
| 2394fcdf35 | |||
| 6edaffeb7e | |||
| 0558b2c595 | |||
| b12bc6042f | |||
| a7bc5df754 | |||
| 1c524c5acc | |||
| 65ad98c20d | |||
| 24d892e322 | |||
| be9b51131d | |||
| 63a65dabe4 | |||
| 1f3b423c22 | |||
| a69a5019f1 | |||
| 5910339bf4 | |||
| 4b641b25a6 | |||
| a847b492bc | |||
| 0df6410561 | |||
| c17faabe9f | |||
| 9a187f9eda | |||
| 2e9452ab7c | |||
| 914745f642 | |||
| 9189bdf50f | |||
| 3e5a97c4b5 | |||
| c0aa6633d2 | |||
| 4e35871861 | |||
| 90f52de147 | |||
| 28b05a82d8 | |||
| b9ed7e2ced | |||
| 5e651d84af | |||
| 6a992a1995 | |||
| 81e886a1ba | |||
| ab9e4a989c | |||
| e350659e24 | |||
| f888a49555 | |||
| 421c249e50 | |||
| a8a6f51387 | |||
| 12e73eef22 | |||
| 2e33fcb16b | |||
| 5912cdd11c | |||
| cc016b3ae3 | |||
| 7760e09462 | |||
| 3ca4456c88 | |||
| 618b7f22da | |||
| a12c7c3b0a | |||
| a42e94c75f | |||
| bc757c8ddb | |||
| 6e11ab9183 | |||
| 2397fcb640 | |||
| 35d0c2a06f | |||
| 813ec74672 |
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask a Question
|
||||
url: https://trigger.dev/discord
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
name: "🧪 E2E Tests"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
e2e:
|
||||
name: "🧪 E2E Tests"
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- 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 ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @references/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
|
||||
@@ -0,0 +1,31 @@
|
||||
name: 🤖 PR Checks
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
uses: ./.github/workflows/typecheck.yml
|
||||
secrets: inherit
|
||||
|
||||
units:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
@@ -38,125 +38,19 @@ env:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: ʦ TypeScript
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: ./.github/workflows/typecheck.yml
|
||||
secrets: inherit
|
||||
|
||||
- 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: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- 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
|
||||
units:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: e2e Tests
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- 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 ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @references/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
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
needs: [typecheck, unitTests, e2e]
|
||||
needs: [typecheck, units, e2e]
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -4,16 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/release.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- ".changeset/**"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: "ʦ TypeScript"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
typecheck:
|
||||
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: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck --filter webapp
|
||||
@@ -0,0 +1,30 @@
|
||||
name: "🧪 Unit Tests"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
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
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"astro-build.astro-vscode",
|
||||
"denoland.vscode-deno"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deno.enablePaths": ["references/deno-reference"]
|
||||
}
|
||||
+23
-4
@@ -32,7 +32,26 @@ Please follow the best-practice of adding changesets in the same commit as the c
|
||||
|
||||
!MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW!
|
||||
|
||||
1. Add changesets as usual `pnpm run changeset:add`
|
||||
2. Create a snapshot version (replace "dev" with your tag) `pnpm exec changeset version --snapshot dev`
|
||||
3. Build the packages: `pnpm run build --filter "@trigger.dev/*"`
|
||||
4. Publish the snapshot (replace "dev" with your tag) `pnpm exec changeset publish --no-git-tag --snapshot --tag dev`
|
||||
1. Add changesets as usual
|
||||
|
||||
```sh
|
||||
pnpm run changeset:add
|
||||
```
|
||||
|
||||
2. Create a snapshot version (replace "prerelease" with your tag)
|
||||
|
||||
```sh
|
||||
pnpm exec changeset version --snapshot prerelease
|
||||
```
|
||||
|
||||
3. Build the packages:
|
||||
|
||||
```sh
|
||||
pnpm run build --filter "@trigger.dev/*"
|
||||
```
|
||||
|
||||
4. Publish the snapshot (replace "dev" with your tag)
|
||||
|
||||
```sh
|
||||
pnpm exec changeset publish --no-git-tag --snapshot --tag prerelease
|
||||
```
|
||||
|
||||
@@ -52,9 +52,13 @@ branch are tagged into a release monthly.
|
||||
Feel free to update `SESSION_SECRET` and `MAGIC_LINK_SECRET` as well using the same method.
|
||||
|
||||
6. Start Docker. This starts the required services like Postgres. If this is your first time using Docker, consider going through this [guide](DOCKER_INSTALLATION.md)
|
||||
|
||||
```
|
||||
pnpm run docker
|
||||
```
|
||||
|
||||
This will also start and run a local instance of [pgAdmin](https://www.pgadmin.org/) on [localhost:5480](http://localhost:5480), preconfigured with email `admin@example.com` and pwd `admin`. Then use `postgres` as the password to the Trigger.dev server.
|
||||
|
||||
7. Migrate the database
|
||||
```
|
||||
pnpm run db:migrate
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
|
||||
</div>
|
||||
|
||||
# ✨🎃 Get involved with Hacktoberfest 2023! 🎃✨
|
||||
|
||||
All of October we're participating in Hacktoberfest and invite you to join us! We have a bunch of issues labeled `🎃 Hacktoberfest` that are ready for you to work on which will count towards Hacktoberfest. We are also running our own game, earn 💎 points to win swag!
|
||||
|
||||
- Check out our [Hacktoberfest landing page](https://trigger.dev/hacktoberfest) for how to participate and win swag.
|
||||
- Contribute to either our [/trigger.dev](https://github.com/triggerdotdev/trigger.dev/labels/%F0%9F%8E%83%20hacktoberfest), [/api-reference](https://github.com/triggerdotdev/api-reference/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%8E%83hacktoberfest) or [/jobs-showcase](https://github.com/triggerdotdev/jobs-showcase/labels/%F0%9F%8E%83%20hacktoberfest) repositories and complete issues marked `🎃 Hacktoberfest` to be eligible for swag.
|
||||
- Join our [Discord](https://discord.gg/JtBAxBr2m3) and get involved in with the community.
|
||||
|
||||
_New to Hacktober? Check out the [Hacktoberfest website](https://hacktoberfest.digitalocean.com/) for more information._
|
||||
|
||||
🎃 **Happy Hacking!** 🎃
|
||||
|
||||
# About Trigger.dev
|
||||
|
||||
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
|
||||
@@ -62,8 +74,8 @@ Click the links to join the discussions about our upcoming features.
|
||||
| Dashboard | View every Task in every Run | ✅ |
|
||||
| Serverless | Long-running Jobs on your serverless backend | ✅ |
|
||||
| React hooks | Easily update your UI with Job progress | ✅ |
|
||||
| React frameworks | Support for Remix, Astro, RedwoodJS & more | ✅ |
|
||||
| [Background tasks](https://github.com/triggerdotdev/trigger.dev/discussions/400) | Offload long or intense Tasks to our infrastructure | 🛠️ |
|
||||
| [React frameworks](https://github.com/triggerdotdev/trigger.dev/discussions/411) | Support for Remix, Astro, RedwoodJS & more | 🛠️ |
|
||||
| [Long-running servers](https://github.com/triggerdotdev/trigger.dev/discussions/430) | Run Jobs on your long-running backend | 🛠️ |
|
||||
| Polling Triggers | Subscribe to changes without webhooks | 🕝 |
|
||||
| Vercel integration | Easy deploy and preview environment support | 🕝 |
|
||||
@@ -83,3 +95,9 @@ We provide an official trigger.dev docker image you can use to easily self-host
|
||||
## Development
|
||||
|
||||
To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md).
|
||||
|
||||
## Meet the Amazing People Behind This Project 🚀
|
||||
|
||||
<a href="https://github.com/triggerdotdev/trigger.dev/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=triggerdotdev/trigger.dev" />
|
||||
</a>
|
||||
|
||||
@@ -44,28 +44,7 @@ export function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey:
|
||||
);
|
||||
}
|
||||
|
||||
export function RunDevCommand() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDevCommand() {
|
||||
export function RunDevCommand({ extra }: { extra?: string }) {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
@@ -77,34 +56,67 @@ export function TriggerDevCommand() {
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npx @trigger.dev/cli@latest dev`}
|
||||
value={`npm run dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm dlx @trigger.dev/cli@latest dev`}
|
||||
value={`pnpm run dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn dlx @trigger.dev/cli@latest dev`}
|
||||
value={`yarn run dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDevStep() {
|
||||
export function TriggerDevCommand({ extra }: { extra?: 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"
|
||||
value={`npx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm dlx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn dlx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDevStep({ extra }: { extra?: string }) {
|
||||
return (
|
||||
<>
|
||||
<Paragraph spacing>
|
||||
In a <span className="text-amber-400">separate terminal window or tab</span> run:
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
<TriggerDevCommand extra={extra} />
|
||||
<Paragraph spacing variant="small">
|
||||
If you’re not running on the default you can specify the port by adding{" "}
|
||||
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
ClientTabsContent,
|
||||
} from "../primitives/ClientTabs";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
|
||||
type InstallPackagesProps = {
|
||||
packages: string[];
|
||||
};
|
||||
|
||||
export function InstallPackages({ packages }: InstallPackagesProps) {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npm install ${packages.join(" ")}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm install ${packages.join(" ")}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn add ${packages.join(" ")}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import type { ViewUpdate } from "@codemirror/view";
|
||||
import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid";
|
||||
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { getEditorSetup } from "./codeMirrorSetup";
|
||||
import { darkTheme } from "./codeMirrorTheme";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
defaultValue?: string;
|
||||
@@ -14,6 +16,8 @@ export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
onChange?: (value: string) => void;
|
||||
onUpdate?: (update: ViewUpdate) => void;
|
||||
onBlur?: (code: string) => void;
|
||||
showCopyButton?: boolean;
|
||||
showClearButton?: boolean;
|
||||
}
|
||||
|
||||
const languages = {
|
||||
@@ -38,6 +42,8 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
onBlur,
|
||||
basicSetup,
|
||||
autoFocus,
|
||||
showCopyButton = true,
|
||||
showClearButton = true,
|
||||
} = {
|
||||
...defaultProps,
|
||||
...opts,
|
||||
@@ -65,7 +71,8 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
onChange,
|
||||
onUpdate,
|
||||
};
|
||||
const { setContainer, state } = useCodeMirror(settings);
|
||||
const { setContainer, view } = useCodeMirror(settings);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
@@ -75,24 +82,71 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
|
||||
//if the defaultValue changes update the editor
|
||||
useEffect(() => {
|
||||
if (state !== undefined) {
|
||||
state.update({
|
||||
changes: { from: 0, to: state.doc.length, insert: defaultValue },
|
||||
if (view !== undefined) {
|
||||
if (view.state.doc.toString() === defaultValue) return;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: defaultValue },
|
||||
});
|
||||
}
|
||||
}, [defaultValue, state]);
|
||||
}, [defaultValue, view]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
if (view === undefined) return;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: undefined },
|
||||
});
|
||||
onChange?.("");
|
||||
}, [view]);
|
||||
|
||||
const copy = useCallback(() => {
|
||||
if (view === undefined) return;
|
||||
navigator.clipboard.writeText(view.state.doc.toString());
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1500);
|
||||
}, [view]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
|
||||
opts.className
|
||||
)}
|
||||
ref={editor}
|
||||
onBlur={() => {
|
||||
if (!onBlur) return;
|
||||
onBlur(editor.current?.textContent ?? "");
|
||||
}}
|
||||
/>
|
||||
<div className={cn(opts.className, "relative")}>
|
||||
<div
|
||||
className="h-full w-full"
|
||||
ref={editor}
|
||||
onBlur={() => {
|
||||
if (!onBlur) return;
|
||||
onBlur(editor.current?.textContent ?? "");
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-3 top-3 flex items-center gap-2">
|
||||
{showClearButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary/small"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clear();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
{showCopyButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary/small"
|
||||
LeadingIcon={copied ? CheckIcon : ClipboardIcon}
|
||||
leadingIconClassName={copied ? "text-green-500 group-hover:text-green-500" : undefined}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import {
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
keymap,
|
||||
} from "@codemirror/view";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import { closeBrackets } from "@codemirror/autocomplete";
|
||||
import { bracketMatching } from "@codemirror/language";
|
||||
import { indentWithTab } from "@codemirror/commands";
|
||||
|
||||
export function getPreviewSetup(): Array<Extension> {
|
||||
return [
|
||||
jsonLang(),
|
||||
highlightSpecialChars(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
highlightSelectionMatches(),
|
||||
lineNumbers(),
|
||||
];
|
||||
}
|
||||
|
||||
export function getViewerSetup(): Array<Extension> {
|
||||
return [drawSelection(), dropCursor(), bracketMatching(), lineNumbers()];
|
||||
}
|
||||
import { jsonParseLinter } from "@codemirror/lang-json";
|
||||
import { bracketMatching } from "@codemirror/language";
|
||||
import { lintGutter, lintKeymap, linter } from "@codemirror/lint";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
import { Prec, type Extension } from "@codemirror/state";
|
||||
import {
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
} from "@codemirror/view";
|
||||
|
||||
export function getEditorSetup(showLineNumbers = true, showHighlights = true): Array<Extension> {
|
||||
const options = [
|
||||
@@ -36,7 +21,20 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
keymap.of([indentWithTab]),
|
||||
lintGutter(),
|
||||
linter(jsonParseLinter()),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{
|
||||
key: "Mod-Enter",
|
||||
run: () => {
|
||||
return true;
|
||||
},
|
||||
preventDefault: false,
|
||||
},
|
||||
])
|
||||
),
|
||||
keymap.of([indentWithTab, ...lintKeymap]),
|
||||
];
|
||||
|
||||
if (showLineNumbers) {
|
||||
|
||||
@@ -17,10 +17,15 @@ export function darkTheme(): Extension {
|
||||
violet = "#c678dd",
|
||||
darkBackground = "#21252b",
|
||||
highlightBackground = "rgba(71,85,105,0.2)",
|
||||
background = "#0f172a",
|
||||
background = "rgba(11, 16, 24 ,100)",
|
||||
tooltipBackground = "#353a42",
|
||||
selection = "rgb(71 85 105)",
|
||||
cursor = "#528bff";
|
||||
cursor = "#528bff",
|
||||
scrollbarTrack = "#0E1521",
|
||||
scrollbarTrackActive = "#131B2B",
|
||||
scrollbarThumb = "#293649",
|
||||
scrollbarThumbActive = "#3C4B62",
|
||||
scrollbarBg = "#0E1521";
|
||||
|
||||
const jsonHeroEditorTheme = EditorView.theme(
|
||||
{
|
||||
@@ -94,6 +99,45 @@ export function darkTheme(): Extension {
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
".cm-scroller": {
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: `${scrollbarThumb} ${scrollbarTrack}`,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar": {
|
||||
display: "block",
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-track": {
|
||||
backgroundColor: scrollbarTrack,
|
||||
borderRadius: "0",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-track:hover": {
|
||||
backgroundColor: scrollbarTrackActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-track:active": {
|
||||
backgroundColor: scrollbarTrackActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: scrollbarThumb,
|
||||
borderRadius: "0",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: scrollbarThumbActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-thumb:active": {
|
||||
backgroundColor: scrollbarThumbActive,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-corner": {
|
||||
backgroundColor: scrollbarBg,
|
||||
borderRadius: "0",
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-corner:hover": {
|
||||
backgroundColor: scrollbarBg,
|
||||
},
|
||||
".cm-scroller::-webkit-scrollbar-corner:active": {
|
||||
backgroundColor: scrollbarBg,
|
||||
},
|
||||
},
|
||||
{ dark: true }
|
||||
);
|
||||
@@ -155,157 +199,3 @@ export function darkTheme(): Extension {
|
||||
|
||||
return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)];
|
||||
}
|
||||
|
||||
export function lightTheme(): Extension[] {
|
||||
const stringColor = "text-[#53a053]",
|
||||
numberColor = "text-[#447bef]",
|
||||
variableColor = "text-[#a42ea2]",
|
||||
booleanColor = "text-[#e2574e]",
|
||||
coral = "text-[#e06c75]",
|
||||
invalid = "text-[#ffffff]",
|
||||
ivory = "text-[#abb2bf]",
|
||||
stone = "text-[#7d8799]",
|
||||
malibu = "text-[#61afef]",
|
||||
whiskey = "text-[#d19a66]",
|
||||
violet = "text-[#c678dd]",
|
||||
darkBackground = "text-[#21252b]",
|
||||
highlightBackground = "text-[#D0D0D0]",
|
||||
background = "text-[#ffffff]",
|
||||
tooltipBackground = "text-[#353a42]",
|
||||
selection = "text-[#D0D0D0]",
|
||||
cursor = "text-[#528bff]";
|
||||
|
||||
const jsonHeroEditorTheme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
color: ivory,
|
||||
backgroundColor: background,
|
||||
},
|
||||
|
||||
".cm-content": {
|
||||
caretColor: cursor,
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
},
|
||||
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
".cm-panels": { backgroundColor: darkBackground, color: ivory },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
|
||||
|
||||
".cm-searchMatch": {
|
||||
backgroundColor: "#72a1ff59",
|
||||
outline: "1px solid #457dff",
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "#6199ff2f",
|
||||
},
|
||||
|
||||
".cm-activeLine": { backgroundColor: highlightBackground },
|
||||
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
|
||||
|
||||
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
|
||||
backgroundColor: "#bad0f847",
|
||||
outline: "1px solid #515a6b",
|
||||
},
|
||||
|
||||
".cm-gutters": {
|
||||
backgroundColor: background,
|
||||
color: stone,
|
||||
border: "none",
|
||||
},
|
||||
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
".cm-foldPlaceholder": {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "#ddd",
|
||||
},
|
||||
|
||||
".cm-tooltip": {
|
||||
border: "none",
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:before": {
|
||||
borderTopColor: "transparent",
|
||||
borderBottomColor: "transparent",
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: highlightBackground,
|
||||
color: ivory,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: false }
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the JSON Hero theme.
|
||||
const jsonHeroHighlightStyle = tagHighlighter([
|
||||
{ tag: tags.keyword, class: violet },
|
||||
{
|
||||
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||
class: variableColor,
|
||||
},
|
||||
{
|
||||
tag: [tags.function(tags.variableName), tags.labelName],
|
||||
class: malibu,
|
||||
},
|
||||
{
|
||||
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
|
||||
class: whiskey,
|
||||
},
|
||||
{ tag: [tags.definition(tags.name), tags.separator], class: ivory },
|
||||
{
|
||||
tag: [
|
||||
tags.typeName,
|
||||
tags.className,
|
||||
tags.number,
|
||||
tags.changed,
|
||||
tags.annotation,
|
||||
tags.modifier,
|
||||
tags.self,
|
||||
tags.namespace,
|
||||
],
|
||||
class: numberColor,
|
||||
},
|
||||
{
|
||||
tag: [
|
||||
tags.operator,
|
||||
tags.operatorKeyword,
|
||||
tags.url,
|
||||
tags.escape,
|
||||
tags.regexp,
|
||||
tags.link,
|
||||
tags.special(tags.string),
|
||||
],
|
||||
class: stringColor,
|
||||
},
|
||||
{ tag: [tags.meta, tags.comment], class: stone },
|
||||
|
||||
{ tag: tags.link, class: stone },
|
||||
{ tag: tags.heading, class: coral },
|
||||
{
|
||||
tag: [tags.atom, tags.bool, tags.special(tags.variableName)],
|
||||
class: booleanColor,
|
||||
},
|
||||
{
|
||||
tag: [tags.processingInstruction, tags.string, tags.inserted],
|
||||
class: stringColor,
|
||||
},
|
||||
{ tag: tags.invalid, class: invalid },
|
||||
]);
|
||||
|
||||
return [jsonHeroEditorTheme, syntaxHighlighting(jsonHeroHighlightStyle)];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { EndpointIndexStatus } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
|
||||
export function EndpointIndexStatusIcon({ status }: { status: EndpointIndexStatus }) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return <ClockIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
case "STARTED":
|
||||
return <Spinner className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
case "SUCCESS":
|
||||
return (
|
||||
<CheckCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />
|
||||
);
|
||||
case "FAILURE":
|
||||
return <XCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function EndpointIndexStatusLabel({ status }: { status: EndpointIndexStatus }) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "STARTED":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "SUCCESS":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "FAILURE":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function endpointIndexStatusTitle(status: EndpointIndexStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "Pending";
|
||||
case "STARTED":
|
||||
return "Started";
|
||||
case "SUCCESS":
|
||||
return "Success";
|
||||
case "FAILURE":
|
||||
return "Failure";
|
||||
}
|
||||
}
|
||||
|
||||
export function endpointIndexStatusClassNameColor(status: EndpointIndexStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "text-dimmed";
|
||||
case "STARTED":
|
||||
return "text-blue-500";
|
||||
case "SUCCESS":
|
||||
return "text-green-500";
|
||||
case "FAILURE":
|
||||
return "text-rose-500";
|
||||
}
|
||||
}
|
||||
@@ -51,28 +51,28 @@ export function FrameworkSelector() {
|
||||
<FrameworkLink to={projectSetupNextjsPath(organization, project)} supported>
|
||||
<NextjsLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupExpressPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupExpressPath(organization, project)} supported>
|
||||
<ExpressLogo className="w-36" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRemixPath(organization, project)} supported>
|
||||
<RemixLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
|
||||
<RedwoodLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupAstroPath(organization, project)} supported>
|
||||
<AstroLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupRedwoodPath(organization, project)}>
|
||||
<RedwoodLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
|
||||
<NuxtLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)} supported>
|
||||
<SvelteKitLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupFastifyPath(organization, project)}>
|
||||
<FastifyLogo className="w-36" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupNestjsPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupNestjsPath(organization, project)} supported>
|
||||
<NestjsLogo className="w-36" />
|
||||
</FrameworkLink>
|
||||
</div>
|
||||
|
||||
@@ -79,37 +79,6 @@ export function HowToRunYourJob() {
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToRunATest() {
|
||||
return (
|
||||
<>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Select an environment
|
||||
"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>Select the environment you’d like the test to run against.</Paragraph>
|
||||
<img src={selectEnvironment} className="mt-2 w-52" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Write your test payload" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Write your own payload specific to your Job. Some Triggers also provide example payloads
|
||||
that you can select from. This will populate the code editor below.
|
||||
</Paragraph>
|
||||
<img src={selectExample} className="mt-2 h-40" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run your test" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>When you’re happy with the payload, click Run test.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Callout variant="docs" to="https://trigger.dev/docs/documentation/guides/testing-jobs">
|
||||
Learn more about running tests.
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToConnectAnIntegration() {
|
||||
return (
|
||||
<>
|
||||
@@ -272,6 +241,21 @@ export function HowToUseApiKeysAndEndpoints() {
|
||||
you should use the Test feature to trigger any scheduled Jobs.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Staging</span>
|
||||
<EnvironmentLabel environment={{ type: "STAGING" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
The <InlineCode>STAGING</InlineCode> environment is where your Jobs will run in a staging
|
||||
environment, meant to mirror your production environment.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
|
||||
@@ -17,7 +17,6 @@ export function NavBar() {
|
||||
<LogoIcon className="h-5 w-5" />
|
||||
</Link>
|
||||
<Breadcrumb />
|
||||
<MobileDropdownMenu />
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
<LinkButton to={docsRoot()} variant="secondary/small" LeadingIcon={BookOpenIcon}>
|
||||
@@ -46,83 +45,3 @@ export function BreadcrumbLink({ title, to }: { title: string; to: string }) {
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileDropdownMenu() {
|
||||
return (
|
||||
<Popover className="block sm:hidden">
|
||||
<Popover.Button
|
||||
className="bg-slate-70 relative z-10 flex h-8 w-8 items-center justify-center rounded border-none bg-opacity-50 focus-visible:border-none focus-visible:outline-none"
|
||||
aria-label="Toggle Navigation"
|
||||
>
|
||||
{({ open }) => <MobileNavIcon open={open} />}
|
||||
</Popover.Button>
|
||||
<Transition.Root>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="duration-150 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-150 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Overlay className="absolute left-0 top-0 h-full w-full origin-top bg-midnight-900/80" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="duration-150 ease-out"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel
|
||||
as="div"
|
||||
className="absolute inset-x-6 top-0 mt-20 flex origin-top flex-col gap-4 rounded-md bg-slate-800 p-4 text-lg tracking-tight text-slate-900 shadow-xl ring-1 ring-slate-700"
|
||||
>
|
||||
{/* <PrimaryA
|
||||
href={docsRoot()}
|
||||
target="_blank"
|
||||
className="max-w-full"
|
||||
>
|
||||
<ArrowTopRightOnSquareIcon className="-ml-1 h-4 w-4" />
|
||||
Documentation
|
||||
</PrimaryA>
|
||||
<PrimaryButton
|
||||
data-attr="posthog-feedback-button"
|
||||
className="max-w-full"
|
||||
>
|
||||
<ChatBubbleLeftRightIcon className="-ml-1 h-4 w-4" />
|
||||
Send us feedback
|
||||
</PrimaryButton>
|
||||
<SecondaryLink to="/logout" className="max-w-full">
|
||||
Logout
|
||||
</SecondaryLink> */}
|
||||
</Popover.Panel>
|
||||
</Transition.Child>
|
||||
</Transition.Root>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNavIcon({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 overflow-visible stroke-slate-300"
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<path
|
||||
d="M0 1H14M0 7H14M0 13H14"
|
||||
className={cn("origin-center transition", open && "scale-90 opacity-0")}
|
||||
/>
|
||||
<path
|
||||
d="M2 2L12 12M12 2L2 12"
|
||||
className={cn("origin-center transition", !open && "scale-90 opacity-0")}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,11 +119,11 @@ export function ProjectSideMenu() {
|
||||
data-action="onboarding"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Homepage"
|
||||
icon="external-link"
|
||||
to="https://trigger.dev"
|
||||
name="Changelog"
|
||||
icon="list"
|
||||
to="https://trigger.dev/changelog"
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="onboarding"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
|
||||
@@ -144,7 +144,7 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
const textColorClassName = variation.textColor;
|
||||
|
||||
return (
|
||||
<div className={cn(fullWidth ? "flex" : "inline-flex text-xxs", btnClassName, className)}>
|
||||
<div className={cn("flex", fullWidth ? "" : "w-fit text-xxs", btnClassName, className)}>
|
||||
<div
|
||||
className={cn(
|
||||
textAlignLeft ? "text-left" : "justify-center",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
export const variantClasses = {
|
||||
info: {
|
||||
@@ -51,8 +52,16 @@ export const variantClasses = {
|
||||
textColor: "text-blue-200",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
pending: {
|
||||
className: "border-blue-400/20 bg-blue-800/30",
|
||||
icon: <Spinner className="h-5 w-5 shrink-0 " />,
|
||||
textColor: "text-blue-300",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type CalloutVariant = keyof typeof variantClasses;
|
||||
|
||||
export function Callout({
|
||||
children,
|
||||
className,
|
||||
@@ -63,7 +72,7 @@ export function Callout({
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
variant: keyof typeof variantClasses;
|
||||
variant: CalloutVariant;
|
||||
to?: string;
|
||||
}) {
|
||||
const variantDefinition = variantClasses[variant];
|
||||
|
||||
@@ -63,16 +63,17 @@ export const DateTimeAccurate = ({ date, timeZone = "UTC" }: DateTimeProps) => {
|
||||
};
|
||||
|
||||
function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string {
|
||||
const milliseconds = `00${date.getMilliseconds()}`.slice(-3);
|
||||
|
||||
const formattedDateTime = new Intl.DateTimeFormat(locales, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
timeZone,
|
||||
// @ts-ignore this works in 92.5% of browsers https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_options_parameter_options_fractionalseconddigits_parameter
|
||||
fractionalSecondDigits: 3,
|
||||
}).format(date);
|
||||
|
||||
return `${formatDateTime}.${milliseconds}`;
|
||||
return formattedDateTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Icon, IconInBox, RenderIcon } from "./Icon";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const variations = {
|
||||
small: {
|
||||
label: {
|
||||
variant: "small" as const,
|
||||
className: "m-0 leading-[1.1rem]",
|
||||
},
|
||||
description: {
|
||||
variant: "extra-small" as const,
|
||||
className: "m-0",
|
||||
},
|
||||
},
|
||||
base: {
|
||||
label: {
|
||||
variant: "base" as const,
|
||||
className: "m-0 leading-[1.1rem] ",
|
||||
},
|
||||
description: {
|
||||
variant: "small" as const,
|
||||
className: "m-0",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type DetailCellProps = {
|
||||
leadingIcon?: RenderIcon;
|
||||
leadingIconClassName?: string;
|
||||
trailingIcon?: RenderIcon;
|
||||
trailingIconClassName?: string;
|
||||
label: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
className?: string;
|
||||
variant?: keyof typeof variations;
|
||||
};
|
||||
|
||||
export function DetailCell({
|
||||
leadingIcon,
|
||||
leadingIconClassName,
|
||||
trailingIcon,
|
||||
trailingIconClassName,
|
||||
label,
|
||||
description,
|
||||
className,
|
||||
variant = "small",
|
||||
}: DetailCellProps) {
|
||||
const variation = variations[variant];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-900",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<IconInBox
|
||||
icon={leadingIcon}
|
||||
className={cn("flex-none transition group-hover:border-slate-750", leadingIconClassName)}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<Paragraph
|
||||
variant={variation.label.variant}
|
||||
className={cn(
|
||||
"flex-1 text-left transition group-hover:text-bright",
|
||||
variation.label.className
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Paragraph>
|
||||
{description && (
|
||||
<Paragraph
|
||||
variant={variation.description.variant}
|
||||
className={cn(
|
||||
"flex-1 text-left text-dimmed transition group-hover:text-bright",
|
||||
variation.description.className
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<Icon
|
||||
icon={trailingIcon}
|
||||
className={cn(
|
||||
"h-6 w-6 flex-none transition group-hover:border-slate-750",
|
||||
trailingIconClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,17 @@ import type { z } from "zod";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function FormError({ children, id }: { children: React.ReactNode; id?: string }) {
|
||||
export function FormError({
|
||||
children,
|
||||
id,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{children && (
|
||||
@@ -11,7 +20,7 @@ export function FormError({ children, id }: { children: React.ReactNode; id?: st
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="flex items-start gap-0.5"
|
||||
className={cn("flex items-start gap-0.5", className)}
|
||||
>
|
||||
<NamedIcon name="error" className="h-4 w-4 shrink-0 justify-start" />
|
||||
<Paragraph id={id} variant="extra-small" className="text-rose-500">
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export type RenderIcon = IconNamesOrString | React.ComponentType<any>;
|
||||
|
||||
type IconProps = {
|
||||
icon?: RenderIcon;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** Use this icon to either render a passed in React component, or a NamedIcon/CompanyIcon */
|
||||
export function Icon(props: IconProps) {
|
||||
if (typeof props.icon === "string") {
|
||||
return <NamedIcon name={props.icon} className={props.className ?? ""} fallback={<></>} />;
|
||||
}
|
||||
|
||||
const Icon = props.icon;
|
||||
|
||||
if (!Icon) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <Icon className={props.className} />;
|
||||
}
|
||||
|
||||
export function IconInBox({ boxClassName, ...props }: IconProps & { boxClassName?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-9 w-9 place-content-center rounded-sm border border-slate-750 bg-slate-850",
|
||||
boxClassName
|
||||
)}
|
||||
>
|
||||
<Icon icon={props.icon} className={cn("h-6 w-6", props.className)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,6 +68,8 @@ import { Spinner } from "./Spinner";
|
||||
import { SaplingIcon } from "~/assets/icons/SaplingIcon";
|
||||
import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon";
|
||||
import { OneTreeIcon } from "~/assets/icons/OneTreeIcon";
|
||||
import { tablerIcons } from "~/utils/tablerIcons";
|
||||
import tablerSpritePath from "./tabler-sprite.svg";
|
||||
|
||||
const icons = {
|
||||
account: (className: string) => <UserCircleIcon className={cn("text-slate-400", className)} />,
|
||||
@@ -131,6 +133,7 @@ const icons = {
|
||||
"clipboard-checked": (className: string) => (
|
||||
<ClipboardDocumentCheckIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
list: (className: string) => <ListBulletIcon className={cn("text-slate-400", className)} />,
|
||||
log: (className: string) => (
|
||||
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
@@ -214,7 +217,15 @@ export function NamedIcon({
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Icon ${name} not found`);
|
||||
if (tablerIcons.has("tabler-" + name)) {
|
||||
return <TablerIcon name={"tabler-" + name} className={className} />;
|
||||
} else if (name.startsWith("tabler-") && tablerIcons.has(name)) {
|
||||
return <TablerIcon name={name} className={className} />;
|
||||
}
|
||||
|
||||
if (name === "supabase-management") {
|
||||
return <NamedIcon name="supabase" className={className} />;
|
||||
}
|
||||
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
@@ -246,3 +257,11 @@ export function NamedIconInBox({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TablerIcon({ name, className }: { name: string; className?: string }) {
|
||||
return (
|
||||
<svg className={cn("stroke-[1.5]", className)}>
|
||||
<use xlinkHref={`${tablerSpritePath}#${name}`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
const isMac = platform === "mac";
|
||||
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const modifiers = relevantShortcut.modifiers ?? [];
|
||||
const character = relevantShortcut.key;
|
||||
const character = keyString(relevantShortcut.key, isMac);
|
||||
|
||||
return (
|
||||
<span className={cn(variants[variant], className)}>
|
||||
@@ -35,6 +35,15 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
);
|
||||
}
|
||||
|
||||
function keyString(key: String, isMac: boolean) {
|
||||
switch (key) {
|
||||
case "enter":
|
||||
return isMac ? "↵" : key;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
function modifierString(modifier: Modifier, isMac: boolean) {
|
||||
switch (modifier) {
|
||||
case "alt":
|
||||
@@ -42,8 +51,10 @@ function modifierString(modifier: Modifier, isMac: boolean) {
|
||||
case "ctrl":
|
||||
return isMac ? "⌃" : "Ctrl+";
|
||||
case "meta":
|
||||
return isMac ? "⌘" : "⊞";
|
||||
return isMac ? "⌘" : "⊞+";
|
||||
case "shift":
|
||||
return isMac ? "⇧" : "Shift+";
|
||||
case "mod":
|
||||
return isMac ? "⌘" : "Ctrl+";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const variations = {
|
||||
container: "flex items-center gap-x-1.5 rounded hover:bg-slate-850 pr-1 py-[0.1rem] pl-1.5",
|
||||
root: "h-3 w-6",
|
||||
thumb: "h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-xs text-slate-400 group-hover:text-slate-200 mt-0.5",
|
||||
text: "text-xs text-slate-400 group-hover:text-slate-200 hover:cursor-pointer",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import toast, { Toaster, resolveValue, useToasterStore } from "react-hot-toast";
|
||||
import { Toaster, toast } from "sonner";
|
||||
|
||||
import { useTypedLoaderData } from "remix-typedjson";
|
||||
import { loader } from "~/root";
|
||||
import { useEffect } from "react";
|
||||
@@ -11,79 +11,55 @@ const permanentToastDuration = 60 * 60 * 24 * 1000;
|
||||
|
||||
export function Toast() {
|
||||
const { toastMessage } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!toastMessage) {
|
||||
return;
|
||||
}
|
||||
const { message, type, options } = toastMessage;
|
||||
|
||||
switch (type) {
|
||||
case "success":
|
||||
toast.success(message, {
|
||||
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
|
||||
});
|
||||
break;
|
||||
case "error":
|
||||
toast.error(message, {
|
||||
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${type} is not handled`);
|
||||
}
|
||||
toast.custom((t) => <ToastUI variant={type} message={message} t={t as string} />, {
|
||||
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
|
||||
});
|
||||
}, [toastMessage]);
|
||||
|
||||
return <Toaster />;
|
||||
}
|
||||
|
||||
export function ToastUI({
|
||||
variant,
|
||||
message,
|
||||
t,
|
||||
toastWidth = 356, // Default width, matches what sonner provides by default
|
||||
}: {
|
||||
variant: "error" | "success";
|
||||
message: string;
|
||||
t: string;
|
||||
toastWidth?: string | number;
|
||||
}) {
|
||||
return (
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
success: {
|
||||
icon: <CheckCircleIcon className="h-6 w-6 text-green-600" />,
|
||||
},
|
||||
error: {
|
||||
icon: <ExclamationCircleIcon className="h-6 w-6 text-rose-600" />,
|
||||
},
|
||||
<div
|
||||
className={`self-end rounded-lg border border-slate-750 bg-midnight-900 shadow-md`}
|
||||
style={{
|
||||
width: toastWidth,
|
||||
}}
|
||||
>
|
||||
{(t) => (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="flex gap-2 rounded-lg border border-slate-750 bg-no-repeat p-4 text-bright shadow-md"
|
||||
style={{
|
||||
opacity: t.visible ? 1 : 0,
|
||||
background:
|
||||
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
|
||||
}}
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
animate={t.visible ? "visible" : "hidden"}
|
||||
variants={{
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.15,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t.icon}
|
||||
{resolveValue(t.message, t)}
|
||||
<button className="p-1" onClick={() => toast.dismiss(t.id)}>
|
||||
<XMarkIcon className="h-4 w-4 text-bright" />
|
||||
</button>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</Toaster>
|
||||
<div
|
||||
className="flex w-full gap-2 rounded-lg bg-no-repeat p-4 text-bright"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
|
||||
}}
|
||||
>
|
||||
{variant === "success" ? (
|
||||
<CheckCircleIcon className="h-6 w-6 text-green-600" />
|
||||
) : (
|
||||
<ExclamationCircleIcon className="h-6 w-6 text-rose-600" />
|
||||
)}
|
||||
{message}
|
||||
<button className="ms-auto p-1" onClick={() => toast.dismiss(t)}>
|
||||
<XMarkIcon className="h-4 w-4 text-bright" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.0 MiB |
@@ -12,7 +12,7 @@ import {
|
||||
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { useMemo } from "react";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { Run } from "~/presenters/RunPresenter.server";
|
||||
import { ViewRun } from "~/presenters/RunPresenter.server";
|
||||
import { cancelSchema } from "~/routes/resources.runs.$runId.cancel";
|
||||
import { schema } from "~/routes/resources.runs.$runId.rerun";
|
||||
import { formatDuration } from "~/utils";
|
||||
@@ -59,7 +59,7 @@ import { TaskCard } from "./TaskCard";
|
||||
import { TaskCardSkeleton } from "./TaskCardSkeleton";
|
||||
|
||||
type RunOverviewProps = {
|
||||
run: Run;
|
||||
run: ViewRun;
|
||||
trigger: {
|
||||
icon: string;
|
||||
title: string;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Fragment, useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Task } from "~/presenters/RunPresenter.server";
|
||||
import { ViewTask } from "~/presenters/RunPresenter.server";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from "./RunCard";
|
||||
import { TaskStatusIcon } from "./TaskStatus";
|
||||
|
||||
type TaskCardProps = Task & {
|
||||
type TaskCardProps = ViewTask & {
|
||||
selectedId?: string;
|
||||
selectedTask: (id: string) => void;
|
||||
isLast: boolean;
|
||||
|
||||
@@ -25,7 +25,7 @@ export function TriggerDetail({
|
||||
};
|
||||
properties: DisplayProperty[];
|
||||
}) {
|
||||
const { id, name, payload, timestamp, deliveredAt } = trigger;
|
||||
const { id, name, payload, context, timestamp, deliveredAt } = trigger;
|
||||
|
||||
return (
|
||||
<RunPanel selected={false}>
|
||||
@@ -45,6 +45,7 @@ export function TriggerDetail({
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconProperty icon="id" label="Event name" value={name} />
|
||||
<RunPanelIconProperty icon="account" label="Event ID" value={id} />
|
||||
{trigger.externalAccount && (
|
||||
<RunPanelIconProperty
|
||||
icon="account"
|
||||
@@ -62,7 +63,9 @@ export function TriggerDetail({
|
||||
</div>
|
||||
)}
|
||||
<Header3>Payload</Header3>
|
||||
<CodeBlock code={JSON.stringify(payload, null, 2)} />
|
||||
<CodeBlock code={payload} />
|
||||
<Header3>Context</Header3>
|
||||
<CodeBlock code={context} />
|
||||
</div>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { DetailCell } from "../primitives/DetailCell";
|
||||
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
|
||||
import { DateTime, DateTimeAccurate } from "../primitives/DateTime";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/DetailCells",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Examples>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => <Examples />,
|
||||
};
|
||||
|
||||
function Examples() {
|
||||
return (
|
||||
<div className="flex max-w-xl flex-col items-start gap-y-8 p-8">
|
||||
<DetailCell
|
||||
leadingIcon="integration"
|
||||
leadingIconClassName="text-dimmed"
|
||||
label="Learn how to create your own API Integrations"
|
||||
variant="base"
|
||||
trailingIcon="external-link"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
<DetailCell
|
||||
leadingIcon={CodeBracketIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
label="Issue comment created"
|
||||
trailingIcon="check"
|
||||
trailingIconClassName="text-green-500 group-hover:text-green-400"
|
||||
/>
|
||||
<DetailCell
|
||||
leadingIcon={ClockIcon}
|
||||
leadingIconClassName="text-slate-400"
|
||||
label={<DateTime date={new Date()} />}
|
||||
description="Run #42 complete"
|
||||
trailingIcon="plus"
|
||||
trailingIconClassName="text-slate-500 group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import { NamedIcon, iconNames } from "../primitives/NamedIcon";
|
||||
import { tablerIcons } from "~/utils/tablerIcons";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
|
||||
const meta: Meta<typeof NamedIcons> = {
|
||||
title: "Icons",
|
||||
@@ -25,17 +27,35 @@ export const Basic: Story = {
|
||||
|
||||
function NamedIcons() {
|
||||
return (
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
{iconNames
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((iconName) => (
|
||||
<div key={iconName} className="flex items-center gap-2">
|
||||
<div>
|
||||
<NamedIcon name={iconName} className={"h-6 w-6"} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Header1 spacing>Internal</Header1>
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
{iconNames
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((iconName) => (
|
||||
<div key={iconName} className="flex items-center gap-2">
|
||||
<div>
|
||||
<NamedIcon name={iconName} className={"h-6 w-6"} />
|
||||
</div>
|
||||
<span className="text-xs text-dimmed">{iconName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Header1 spacing>Tabler</Header1>
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
{Array.from(tablerIcons).map((iconName) => (
|
||||
<div key={iconName} className="flex items-center gap-2">
|
||||
<div>
|
||||
<NamedIcon name={iconName} className={"h-6 w-6 text-indigo-500"} />
|
||||
</div>
|
||||
<span className="text-xs text-dimmed">{iconName}</span>
|
||||
</div>
|
||||
<span className="text-xs text-dimmed">{iconName}</span>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ const shortcuts: ShortcutDefinition[] = [
|
||||
{ key: "f", modifiers: ["meta"] },
|
||||
{ key: "k", modifiers: ["meta"] },
|
||||
{ key: "del", modifiers: ["ctrl", "alt"] },
|
||||
{ key: "enter", modifiers: ["meta"] },
|
||||
{ key: "enter", modifiers: ["mod"] },
|
||||
];
|
||||
|
||||
function Collection() {
|
||||
@@ -67,6 +69,9 @@ function Set({ platform }: { platform: "mac" | "windows" }) {
|
||||
<Button variant="danger/medium" shortcut={shortcut}>
|
||||
Danger medium
|
||||
</Button>
|
||||
<Button variant="danger/medium" shortcut={shortcut}>
|
||||
Danger medium
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</OperatingSystemContextProvider>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { ToastUI } from "../primitives/Toast";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/Toast",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Collection>;
|
||||
|
||||
export const Toasts: Story = {
|
||||
render: () => <Collection />,
|
||||
};
|
||||
|
||||
function Collection() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-4 p-4">
|
||||
<ToastUI variant="success" message="Success UI" t="-" />
|
||||
<ToastUI variant="error" message="Error UI" t="-" />
|
||||
<br />
|
||||
<Button
|
||||
variant="primary/large"
|
||||
onClick={() =>
|
||||
toast.custom((t) => <ToastUI variant="success" message="Success" t={t as string} />, {
|
||||
duration: Infinity, // Prevents auto-dismissal for demo purposes
|
||||
})
|
||||
}
|
||||
>
|
||||
Success
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary/large"
|
||||
onClick={() =>
|
||||
toast.custom((t) => <ToastUI variant="error" message="Error" t={t as string} />, {
|
||||
duration: Infinity,
|
||||
})
|
||||
}
|
||||
>
|
||||
Error
|
||||
</Button>
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,3 +5,7 @@ export const DEFAULT_MAX_CONCURRENT_RUNS = 10;
|
||||
export const MAX_CONCURRENT_RUNS_LIMIT = 20;
|
||||
export const PREPROCESS_RETRY_LIMIT = 2;
|
||||
export const EXECUTE_JOB_RETRY_LIMIT = 10;
|
||||
export const MAX_RUN_YIELDED_EXECUTIONS = 100;
|
||||
export const RUN_CHUNK_EXECUTION_BUFFER = 350;
|
||||
export const MAX_RUN_CHUNK_EXECUTION_LIMIT = 120000; // 2 minutes
|
||||
export const RESPONSE_TIMEOUT_STATUS_CODES = [408, 504];
|
||||
|
||||
@@ -31,7 +31,7 @@ export type PrismaTransactionOptions = {
|
||||
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
|
||||
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||
|
||||
rethrowPrismaErrors?: boolean;
|
||||
swallowPrismaErrors?: boolean;
|
||||
};
|
||||
|
||||
export async function $transaction<R>(
|
||||
@@ -55,11 +55,9 @@ export async function $transaction<R>(
|
||||
name: error.name,
|
||||
});
|
||||
|
||||
if (options?.rethrowPrismaErrors) {
|
||||
throw error;
|
||||
if (options?.swallowPrismaErrors) {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -124,6 +122,10 @@ function getClient() {
|
||||
emit: "stdout",
|
||||
level: "warn",
|
||||
},
|
||||
// {
|
||||
// emit: "stdout",
|
||||
// level: "query",
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const EnvironmentSchema = z.object({
|
||||
EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
|
||||
WORKER_ENABLED: z.string().default("true"),
|
||||
EXECUTION_WORKER_ENABLED: z.string().default("true"),
|
||||
GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { MatchedProject, useOptionalProject } from "./useProject";
|
||||
import { useUser } from "./useUser";
|
||||
|
||||
export type ProjectJobEnvironment = MatchedProject["environments"][number];
|
||||
|
||||
@@ -11,10 +12,13 @@ export function useEnvironments(matches?: RouteMatch[]) {
|
||||
}
|
||||
|
||||
export function useDevEnvironment(matches?: RouteMatch[]) {
|
||||
const user = useUser();
|
||||
const environments = useEnvironments(matches);
|
||||
if (!environments) return;
|
||||
|
||||
return environments.find((environment) => environment.type === "DEVELOPMENT");
|
||||
return environments.find(
|
||||
(environment) => environment.type === "DEVELOPMENT" && environment.userId === user.id
|
||||
);
|
||||
}
|
||||
|
||||
export function useProdEnvironment(matches?: RouteMatch[]) {
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { ProjectJob } from "./useJobs";
|
||||
import { useTextFilter } from "./useTextFilter";
|
||||
import { useToggleFilter } from "./useToggleFilter";
|
||||
|
||||
export function useFilterJobs(jobs: ProjectJob[]) {
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<ProjectJob>({
|
||||
export function useFilterJobs(jobs: ProjectJob[], onlyActiveJobs = false) {
|
||||
const toggleFilterRes = useToggleFilter<ProjectJob>({
|
||||
items: jobs,
|
||||
filter: (job, onlyActiveJobs) => {
|
||||
if (onlyActiveJobs && job.status !== "ACTIVE") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
defaultValue: onlyActiveJobs,
|
||||
});
|
||||
|
||||
const textFilterRes = useTextFilter<ProjectJob>({
|
||||
items: toggleFilterRes.filteredItems,
|
||||
filter: (job, text) => {
|
||||
if (job.slug.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (job.title.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
@@ -24,5 +36,11 @@ export function useFilterJobs(jobs: ProjectJob[]) {
|
||||
},
|
||||
});
|
||||
|
||||
return { filterText, setFilterText, filteredItems };
|
||||
return {
|
||||
filteredItems: textFilterRes.filteredItems,
|
||||
filterText: textFilterRes.filterText,
|
||||
setFilterText: textFilterRes.setFilterText,
|
||||
onlyActiveJobs: toggleFilterRes.isToggleActive,
|
||||
setOnlyActiveJobs: toggleFilterRes.setToggleActive,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider";
|
||||
|
||||
export type Modifier = "alt" | "ctrl" | "meta" | "shift";
|
||||
export type Modifier = "alt" | "ctrl" | "meta" | "shift" | "mod";
|
||||
|
||||
export type Shortcut = {
|
||||
key: string;
|
||||
modifiers?: Modifier[];
|
||||
enabledOnInputElements?: boolean;
|
||||
};
|
||||
|
||||
export type ShortcutDefinition =
|
||||
@@ -20,19 +20,31 @@ type useShortcutKeysProps = {
|
||||
shortcut: ShortcutDefinition;
|
||||
action: (event: KeyboardEvent) => void;
|
||||
disabled?: boolean;
|
||||
enabledOnInputElements?: boolean;
|
||||
};
|
||||
|
||||
export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) {
|
||||
const keys = createKeysFromShortcut(shortcut);
|
||||
useHotkeys(keys, action, { enabled: !disabled });
|
||||
}
|
||||
|
||||
function createKeysFromShortcut(shortcut: ShortcutDefinition) {
|
||||
const { platform } = useOperatingSystem();
|
||||
const isMac = platform === "mac";
|
||||
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const modifiers = relevantShortcut.modifiers;
|
||||
const character = relevantShortcut.key;
|
||||
const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
|
||||
return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character;
|
||||
const keys = createKeysFromShortcut(relevantShortcut);
|
||||
useHotkeys(
|
||||
keys,
|
||||
(event, hotkeysEvent) => {
|
||||
action(event);
|
||||
},
|
||||
{
|
||||
enabled: !disabled,
|
||||
enableOnFormTags: relevantShortcut.enabledOnInputElements,
|
||||
enableOnContentEditable: relevantShortcut.enabledOnInputElements,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createKeysFromShortcut(shortcut: Shortcut) {
|
||||
const modifiers = shortcut.modifiers;
|
||||
const character = shortcut.key;
|
||||
|
||||
return modifiers ? modifiers.map((k) => k).join("+") + "+" + character : character;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type ToggleFilterProps<T> = {
|
||||
items: T[];
|
||||
filter: (item: T, isToggleActive: boolean) => boolean;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
|
||||
export function useToggleFilter<T>({ items, filter, defaultValue = false }: ToggleFilterProps<T>) {
|
||||
const [isToggleActive, setToggleActive] = useState(defaultValue);
|
||||
|
||||
const filteredItems = useMemo<T[]>(() => {
|
||||
return items.filter((item) => filter(item, isToggleActive));
|
||||
}, [items, isToggleActive]);
|
||||
|
||||
return {
|
||||
isToggleActive,
|
||||
setToggleActive,
|
||||
filteredItems,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RESPONSE_TIMEOUT_STATUS_CODES } from "~/consts";
|
||||
import { prisma } from "~/db.server";
|
||||
import { Prettify } from "~/lib.es5";
|
||||
|
||||
@@ -18,3 +19,14 @@ export async function findEndpoint(id: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function detectResponseIsTimeout(response?: Response) {
|
||||
if (!response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
RESPONSE_TIMEOUT_STATUS_CODES.includes(response.status) ||
|
||||
response.headers.get("x-vercel-error") === "FUNCTION_INVOCATION_TIMEOUT"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const IndexEndpointStatsSchema = z.object({
|
||||
jobs: z.number(),
|
||||
sources: z.number(),
|
||||
dynamicTriggers: z.number(),
|
||||
dynamicSchedules: z.number(),
|
||||
});
|
||||
|
||||
export type IndexEndpointStats = z.infer<typeof IndexEndpointStatsSchema>;
|
||||
|
||||
export function parseEndpointIndexStats(stats: unknown): IndexEndpointStats {
|
||||
return IndexEndpointStatsSchema.parse(stats);
|
||||
}
|
||||
@@ -2,27 +2,6 @@ import { JobRun, JobRunExecution } from "@trigger.dev/database";
|
||||
import { PrismaClientOrTransaction } from "~/db.server";
|
||||
import { executionWorker } from "~/services/worker.server";
|
||||
|
||||
export async function enqueueRunExecutionV1(
|
||||
execution: JobRunExecution,
|
||||
queueId: string,
|
||||
concurrency: number,
|
||||
tx: PrismaClientOrTransaction,
|
||||
runAt?: Date
|
||||
) {
|
||||
const job = await executionWorker.enqueue(
|
||||
"performRunExecution",
|
||||
{
|
||||
id: execution.id,
|
||||
},
|
||||
{
|
||||
queueName: `job:queue:${queueId}`,
|
||||
tx,
|
||||
runAt,
|
||||
jobKey: `execution:${execution.runId}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export type EnqueueRunExecutionV2Options = {
|
||||
runAt?: Date;
|
||||
resumeTaskId?: string;
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
import { customAlphabet } from "nanoid";
|
||||
import slug from "slug";
|
||||
import { prisma, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { createProject } from "./project.server";
|
||||
|
||||
export type { Organization };
|
||||
@@ -176,10 +175,10 @@ function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
return "prod";
|
||||
}
|
||||
case "STAGING": {
|
||||
return "staging";
|
||||
return "stg";
|
||||
}
|
||||
case "PREVIEW": {
|
||||
return "preview";
|
||||
return "prev";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function createProject(
|
||||
|
||||
// Create the dev and prod environments
|
||||
await createEnvironment(organization, project, "PRODUCTION");
|
||||
await createEnvironment(organization, project, "STAGING");
|
||||
|
||||
for (const member of project.organization.members) {
|
||||
await createEnvironment(organization, project, "DEVELOPMENT", member);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Task, TaskAttempt } from "@trigger.dev/database";
|
||||
import { ServerTask } from "@trigger.dev/core";
|
||||
import type { JobRun, Task, TaskAttempt } from "@trigger.dev/database";
|
||||
import { CachedTask, ServerTask } from "@trigger.dev/core";
|
||||
|
||||
export type TaskWithAttempts = Task & { attempts: TaskAttempt[] };
|
||||
export type TaskWithAttempts = Task & { attempts: TaskAttempt[]; run: JobRun };
|
||||
|
||||
export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask {
|
||||
return {
|
||||
@@ -23,5 +23,91 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
|
||||
attempts: task.attempts.length,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
operation: task.operation,
|
||||
callbackUrl: task.callbackUrl,
|
||||
forceYield: task.run.forceYieldImmediately,
|
||||
};
|
||||
}
|
||||
|
||||
export type TaskForCaching = Pick<
|
||||
Task,
|
||||
"id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId"
|
||||
>;
|
||||
|
||||
export function prepareTasksForCaching(
|
||||
possibleTasks: TaskForCaching[],
|
||||
maxSize: number
|
||||
): {
|
||||
tasks: CachedTask[];
|
||||
cursor: string | undefined;
|
||||
} {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && !task.noop);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = maxSize;
|
||||
|
||||
for (const task of tasks) {
|
||||
const cachedTask = prepareTaskForCaching(task);
|
||||
const size = calculateCachedTaskSize(cachedTask);
|
||||
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(cachedTask);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: tasksToRun,
|
||||
cursor: tasks.length > tasksToRun.length ? tasks[tasksToRun.length].id : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareTasksForCachingLegacy(
|
||||
possibleTasks: TaskForCaching[],
|
||||
maxSize: number
|
||||
): {
|
||||
tasks: CachedTask[];
|
||||
cursor: string | undefined;
|
||||
} {
|
||||
const tasks = possibleTasks.filter((task) => task.status === "COMPLETED");
|
||||
|
||||
// Prepare tasks and calculate their sizes
|
||||
const availableTasks = tasks.map((task) => {
|
||||
const cachedTask = prepareTaskForCaching(task);
|
||||
return { task: cachedTask, size: calculateCachedTaskSize(cachedTask) };
|
||||
});
|
||||
|
||||
// Sort tasks in ascending order by size
|
||||
availableTasks.sort((a, b) => a.size - b.size);
|
||||
|
||||
// Select tasks using greedy approach
|
||||
const tasksToRun: CachedTask[] = [];
|
||||
let remainingSize = maxSize;
|
||||
|
||||
for (const { task, size } of availableTasks) {
|
||||
if (size <= remainingSize) {
|
||||
tasksToRun.push(task);
|
||||
remainingSize -= size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: tasksToRun,
|
||||
cursor: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function prepareTaskForCaching(task: TaskForCaching): CachedTask {
|
||||
return {
|
||||
id: task.idempotencyKey, // We should eventually move this back to task.id
|
||||
status: task.status,
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
noop: task.noop,
|
||||
output: task.output as any,
|
||||
parentId: task.parentId,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateCachedTaskSize(task: CachedTask): number {
|
||||
return JSON.stringify(task).length;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { run as graphileRun, parseCronItems } from "graphile-worker";
|
||||
import omit from "lodash.omit";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { workerLogger as logger } from "~/services/logger.server";
|
||||
|
||||
export interface MessageCatalogSchema {
|
||||
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
|
||||
@@ -81,6 +81,18 @@ export type ZodWorkerDequeueOptions = {
|
||||
tx?: PrismaClientOrTransaction;
|
||||
};
|
||||
|
||||
const CLEANUP_TASK_NAME = "__cleanupOldJobs";
|
||||
const REPORTER_TASK_NAME = "__reporter";
|
||||
|
||||
export type ZodWorkerCleanupOptions = {
|
||||
frequencyExpression: string; // cron expression
|
||||
ttl: number;
|
||||
maxCount: number;
|
||||
taskOptions?: CronItemOptions;
|
||||
};
|
||||
|
||||
type ZodWorkerReporter = (event: string, properties: Record<string, any>) => Promise<void>;
|
||||
|
||||
export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
|
||||
name: string;
|
||||
runnerOptions: RunnerOptions;
|
||||
@@ -88,6 +100,9 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
|
||||
schema: TMessageCatalog;
|
||||
tasks: ZodTasks<TMessageCatalog>;
|
||||
recurringTasks?: ZodRecurringTasks;
|
||||
cleanup?: ZodWorkerCleanupOptions;
|
||||
reporter?: ZodWorkerReporter;
|
||||
shutdownTimeoutInMs?: number;
|
||||
};
|
||||
|
||||
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
@@ -98,6 +113,10 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
#tasks: ZodTasks<TMessageCatalog>;
|
||||
#recurringTasks?: ZodRecurringTasks;
|
||||
#runner?: GraphileRunner;
|
||||
#cleanup: ZodWorkerCleanupOptions | undefined;
|
||||
#reporter?: ZodWorkerReporter;
|
||||
#shutdownTimeoutInMs?: number;
|
||||
#shuttingDown = false;
|
||||
|
||||
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
|
||||
this.#name = options.name;
|
||||
@@ -106,6 +125,9 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
this.#runnerOptions = options.runnerOptions;
|
||||
this.#tasks = options.tasks;
|
||||
this.#recurringTasks = options.recurringTasks;
|
||||
this.#cleanup = options.cleanup;
|
||||
this.#reporter = options.reporter;
|
||||
this.#shutdownTimeoutInMs = options.shutdownTimeoutInMs ?? 60000; // default to 60 seconds
|
||||
}
|
||||
|
||||
get graphileWorkerSchema() {
|
||||
@@ -125,6 +147,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
|
||||
this.#runner = await graphileRun({
|
||||
...this.#runnerOptions,
|
||||
noHandleSignals: true,
|
||||
taskList: this.#createTaskListFromTasks(),
|
||||
parsedCronItems,
|
||||
});
|
||||
@@ -181,9 +204,34 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
this.#logDebug("stop");
|
||||
});
|
||||
|
||||
process.on("SIGTERM", this._handleSignal.bind(this));
|
||||
process.on("SIGINT", this._handleSignal.bind(this));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleSignal(signal: string) {
|
||||
if (this.#shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#shuttingDown = true;
|
||||
|
||||
if (this.#shutdownTimeoutInMs) {
|
||||
setTimeout(() => {
|
||||
this.#logDebug("Shutdown timeout reached, exiting process");
|
||||
|
||||
process.exit(0);
|
||||
}, this.#shutdownTimeoutInMs);
|
||||
}
|
||||
|
||||
this.#logDebug(`Received ${signal}, shutting down zodWorker...`);
|
||||
|
||||
this.stop().finally(() => {
|
||||
this.#logDebug("zodWorker stopped");
|
||||
});
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.#runner?.stop();
|
||||
}
|
||||
@@ -337,12 +385,45 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
taskList[key] = task;
|
||||
}
|
||||
|
||||
if (this.#cleanup) {
|
||||
const task: Task = (payload, helpers) => {
|
||||
return this.#handleCleanup(payload, helpers);
|
||||
};
|
||||
|
||||
taskList[CLEANUP_TASK_NAME] = task;
|
||||
}
|
||||
|
||||
if (this.#reporter) {
|
||||
const task: Task = (payload, helpers) => {
|
||||
return this.#handleReporter(payload, helpers);
|
||||
};
|
||||
|
||||
taskList[REPORTER_TASK_NAME] = task;
|
||||
}
|
||||
|
||||
return taskList;
|
||||
}
|
||||
|
||||
#createCronItemsFromRecurringTasks() {
|
||||
const cronItems: CronItem[] = [];
|
||||
|
||||
if (this.#cleanup) {
|
||||
cronItems.push({
|
||||
pattern: this.#cleanup.frequencyExpression,
|
||||
identifier: CLEANUP_TASK_NAME,
|
||||
task: CLEANUP_TASK_NAME,
|
||||
options: this.#cleanup.taskOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.#reporter) {
|
||||
cronItems.push({
|
||||
pattern: "50 * * * *", // Every hour at 50 minutes past the hour
|
||||
identifier: REPORTER_TASK_NAME,
|
||||
task: REPORTER_TASK_NAME,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.#recurringTasks) {
|
||||
return cronItems;
|
||||
}
|
||||
@@ -434,6 +515,112 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
}
|
||||
}
|
||||
|
||||
async #handleCleanup(rawPayload: unknown, helpers: JobHelpers): Promise<void> {
|
||||
if (!this.#cleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const job = helpers.job;
|
||||
|
||||
logger.debug("Received cleanup task", {
|
||||
payload: rawPayload,
|
||||
job,
|
||||
});
|
||||
|
||||
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error(
|
||||
`Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}`
|
||||
);
|
||||
}
|
||||
|
||||
const payload = parsedPayload.data;
|
||||
|
||||
// Add the this.#cleanup.ttl to the payload._cron.ts
|
||||
const expirationDate = new Date(payload._cron.ts.getTime() - this.#cleanup.ttl);
|
||||
|
||||
logger.debug("Cleaning up old jobs", {
|
||||
expirationDate,
|
||||
payload,
|
||||
});
|
||||
|
||||
const rawResults = await this.#prisma.$queryRawUnsafe(
|
||||
`WITH rows AS (SELECT id FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts LIMIT $2 FOR UPDATE) DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE id IN (SELECT id FROM rows) RETURNING id`,
|
||||
expirationDate,
|
||||
this.#cleanup.maxCount
|
||||
);
|
||||
|
||||
const results = Array.isArray(rawResults) ? rawResults : [];
|
||||
|
||||
logger.debug("Cleaned up old jobs", {
|
||||
count: results.length,
|
||||
expirationDate,
|
||||
payload,
|
||||
});
|
||||
|
||||
if (this.#reporter) {
|
||||
await this.#reporter("cleanup_stats", {
|
||||
count: results.length,
|
||||
expirationDate,
|
||||
ts: payload._cron.ts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #handleReporter(rawPayload: unknown, helpers: JobHelpers): Promise<void> {
|
||||
if (!this.#reporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Received reporter task", {
|
||||
payload: rawPayload,
|
||||
});
|
||||
|
||||
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error(
|
||||
`Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}`
|
||||
);
|
||||
}
|
||||
|
||||
const payload = parsedPayload.data;
|
||||
|
||||
// Subtract an hour from the payload._cron.ts
|
||||
const startAt = new Date(payload._cron.ts.getTime() - 1000 * 60 * 60);
|
||||
|
||||
const schema = z.array(z.object({ count: z.coerce.number() }));
|
||||
|
||||
// Count the number of jobs that have been added since the startAt date and before the payload._cron.ts date
|
||||
const rawAddedResults = await this.#prisma.$queryRawUnsafe(
|
||||
`SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs WHERE created_at > $1 AND created_at < $2`,
|
||||
startAt,
|
||||
payload._cron.ts
|
||||
);
|
||||
|
||||
const addedCountResults = schema.parse(rawAddedResults)[0];
|
||||
|
||||
// Count the total number of jobs in the jobs table
|
||||
const rawTotalResults = await this.#prisma.$queryRawUnsafe(
|
||||
`SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs`
|
||||
);
|
||||
|
||||
const totalCountResults = schema.parse(rawTotalResults)[0];
|
||||
|
||||
logger.debug("Calculated metrics about the jobs table", {
|
||||
rawAddedResults,
|
||||
rawTotalResults,
|
||||
payload,
|
||||
});
|
||||
|
||||
await this.#reporter("queue_metrics", {
|
||||
addedCount: addedCountResults.count,
|
||||
totalCount: totalCountResults.count,
|
||||
ts: payload._cron.ts,
|
||||
});
|
||||
}
|
||||
|
||||
#logDebug(message: string, args?: any) {
|
||||
logger.debug(`[worker][${this.#name}] ${message}`, args);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Job } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
|
||||
type ApiRunOptions = {
|
||||
runId: Job["id"];
|
||||
maxTasks?: number;
|
||||
taskDetails?: boolean;
|
||||
subTasks?: boolean;
|
||||
cursor?: string;
|
||||
};
|
||||
|
||||
export class ApiRunPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
runId,
|
||||
maxTasks = 20,
|
||||
taskDetails = false,
|
||||
subTasks = false,
|
||||
cursor,
|
||||
}: ApiRunOptions) {
|
||||
const take = Math.min(maxTasks, 50);
|
||||
|
||||
return await prisma.jobRun.findUnique({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
startedAt: true,
|
||||
updatedAt: true,
|
||||
completedAt: true,
|
||||
environmentId: true,
|
||||
output: true,
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
displayKey: true,
|
||||
status: true,
|
||||
name: true,
|
||||
icon: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
params: taskDetails,
|
||||
output: taskDetails,
|
||||
},
|
||||
where: {
|
||||
parentId: subTasks ? undefined : null,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
take: take + 1,
|
||||
cursor: cursor
|
||||
? {
|
||||
id: cursor,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
statuses: {
|
||||
select: { key: true, label: true, state: true, data: true, history: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import {
|
||||
import type {
|
||||
Endpoint,
|
||||
EndpointIndex,
|
||||
EndpointIndexStatus,
|
||||
RuntimeEnvironment,
|
||||
RuntimeEnvironmentType,
|
||||
} from "../../../../packages/database/src";
|
||||
import { env } from "~/env.server";
|
||||
} from "@trigger.dev/database";
|
||||
import {
|
||||
EndpointIndexError,
|
||||
EndpointIndexErrorSchema,
|
||||
IndexEndpointStats,
|
||||
parseEndpointIndexStats,
|
||||
} from "@trigger.dev/core";
|
||||
|
||||
export type Client = {
|
||||
slug: string;
|
||||
endpoints: {
|
||||
DEVELOPMENT: ClientEndpoint;
|
||||
PRODUCTION: ClientEndpoint;
|
||||
STAGING?: ClientEndpoint;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,9 +40,11 @@ export type ClientEndpoint =
|
||||
url: string;
|
||||
indexWebhookPath: string;
|
||||
latestIndex?: {
|
||||
status: EndpointIndexStatus;
|
||||
source: string;
|
||||
updatedAt: Date;
|
||||
stats: IndexEndpointStats;
|
||||
stats?: IndexEndpointStats;
|
||||
error?: EndpointIndexError;
|
||||
};
|
||||
environment: {
|
||||
id: string;
|
||||
@@ -81,9 +89,11 @@ export class EnvironmentsPresenter {
|
||||
indexingHookIdentifier: true,
|
||||
indexings: {
|
||||
select: {
|
||||
status: true,
|
||||
source: true,
|
||||
updatedAt: true,
|
||||
stats: true,
|
||||
error: true,
|
||||
},
|
||||
take: 1,
|
||||
orderBy: {
|
||||
@@ -133,6 +143,8 @@ export class EnvironmentsPresenter {
|
||||
throw new Error("Development environment not found, this should not happen");
|
||||
}
|
||||
|
||||
const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING");
|
||||
|
||||
const productionEnvironment = filtered.find(
|
||||
(environment) => environment.type === "PRODUCTION"
|
||||
);
|
||||
@@ -151,6 +163,9 @@ export class EnvironmentsPresenter {
|
||||
state: "unconfigured",
|
||||
environment: productionEnvironment,
|
||||
},
|
||||
STAGING: stagingEnvironment
|
||||
? { state: "unconfigured", environment: stagingEnvironment }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -161,6 +176,16 @@ export class EnvironmentsPresenter {
|
||||
client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl);
|
||||
}
|
||||
|
||||
if (stagingEnvironment) {
|
||||
const stagingEndpoint = stagingEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
|
||||
if (stagingEndpoint) {
|
||||
client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const prodEndpoint = productionEnvironment.endpoints.find(
|
||||
(endpoint) => endpoint.slug === slug
|
||||
);
|
||||
@@ -199,7 +224,7 @@ const environmentSortOrder: RuntimeEnvironmentType[] = [
|
||||
|
||||
function endpointClient(
|
||||
endpoint: Pick<Endpoint, "id" | "slug" | "url" | "indexingHookIdentifier"> & {
|
||||
indexings: Pick<EndpointIndex, "source" | "updatedAt" | "stats">[];
|
||||
indexings: Pick<EndpointIndex, "status" | "source" | "updatedAt" | "stats" | "error">[];
|
||||
},
|
||||
environment: Pick<RuntimeEnvironment, "id" | "apiKey" | "type">,
|
||||
baseUrl: string
|
||||
@@ -212,9 +237,13 @@ function endpointClient(
|
||||
indexWebhookPath: `${baseUrl}/api/v1/endpoints/${environment.id}/${endpoint.slug}/index/${endpoint.indexingHookIdentifier}`,
|
||||
latestIndex: endpoint.indexings[0]
|
||||
? {
|
||||
status: endpoint.indexings[0].status,
|
||||
source: endpoint.indexings[0].source,
|
||||
updatedAt: endpoint.indexings[0].updatedAt,
|
||||
stats: parseEndpointIndexStats(endpoint.indexings[0].stats),
|
||||
error: endpoint.indexings[0].error
|
||||
? EndpointIndexErrorSchema.parse(endpoint.indexings[0].error)
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
environment: environment,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
export class OrgUsagePresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -33,6 +34,7 @@ export class OrgUsagePresenter {
|
||||
createdAt: {
|
||||
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
},
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,6 +46,7 @@ export class OrgUsagePresenter {
|
||||
gte: startOfLastMonth,
|
||||
lt: startOfMonth,
|
||||
},
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,7 +66,7 @@ export class OrgUsagePresenter {
|
||||
month: string;
|
||||
count: number;
|
||||
}[]
|
||||
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' GROUP BY month ORDER BY month ASC`;
|
||||
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' AND "internal" = FALSE GROUP BY month ORDER BY month ASC`;
|
||||
|
||||
const chartData = chartDataRaw.map((obj) => ({
|
||||
name: obj.month,
|
||||
@@ -139,11 +142,13 @@ export class OrgUsagePresenter {
|
||||
},
|
||||
});
|
||||
|
||||
const chartDataDisplay = fillInMissingMonthlyData(chartData, 6);
|
||||
|
||||
return {
|
||||
id: organization.id,
|
||||
runsCount,
|
||||
runsCountLastMonth,
|
||||
chartData: fillInMissingMonthlyData(chartData, 6),
|
||||
chartData: chartDataDisplay,
|
||||
totalJobs,
|
||||
totalJobsLastMonth,
|
||||
totalIntegrations,
|
||||
@@ -166,7 +171,7 @@ function fillInMissingMonthlyData(
|
||||
|
||||
const startMonth = new Date(
|
||||
new Date(currentMonth).getFullYear(),
|
||||
new Date(currentMonth).getMonth() - totalNumberOfMonths,
|
||||
new Date(currentMonth).getMonth() - (totalNumberOfMonths - 2),
|
||||
1
|
||||
)
|
||||
.toISOString()
|
||||
@@ -182,17 +187,36 @@ function fillInMissingMonthlyData(
|
||||
return completeData;
|
||||
}
|
||||
|
||||
// Start month will be like 2023-03 and endMonth will be like 2023-10
|
||||
// The result should be an array of months between these two months, including the start and end month
|
||||
// So for example, if startMonth is 2023-03 and endMonth is 2023-10, the result should be:
|
||||
// ["2023-03", "2023-04", "2023-05", "2023-06", "2023-07", "2023-08", "2023-09", "2023-10"]
|
||||
function getMonthsBetween(startMonth: string, endMonth: string): string[] {
|
||||
const startDate = new Date(startMonth);
|
||||
const endDate = new Date(endMonth);
|
||||
// Initialize result array
|
||||
const result: string[] = [];
|
||||
|
||||
const months = [];
|
||||
let currentDate = startDate;
|
||||
// Parse the year and month from startMonth and endMonth
|
||||
let [startYear, startMonthNum] = startMonth.split("-").map(Number);
|
||||
let [endYear, endMonthNum] = endMonth.split("-").map(Number);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
months.push(currentDate.toISOString().slice(0, 7));
|
||||
currentDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1));
|
||||
// Loop through each month between startMonth and endMonth
|
||||
for (let year = startYear; year <= endYear; year++) {
|
||||
let monthStart = year === startYear ? startMonthNum : 1;
|
||||
let monthEnd = year === endYear ? endMonthNum : 12;
|
||||
|
||||
for (let month = monthStart; month <= monthEnd; month++) {
|
||||
// Format the month into a string and add it to the result array
|
||||
result.push(`${year}-${String(month).padStart(2, "0")}`);
|
||||
}
|
||||
}
|
||||
|
||||
return months;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getLastSecondOfMonth(endMonth: string) {
|
||||
const [year, month] = endMonth.split("-").map(Number);
|
||||
const nextMonthFirstDay = new Date(year, month, 1);
|
||||
nextMonthFirstDay.setDate(0);
|
||||
nextMonthFirstDay.setHours(23, 59, 59);
|
||||
return nextMonthFirstDay;
|
||||
}
|
||||
|
||||
@@ -100,21 +100,24 @@ export class RunListPresenter {
|
||||
let previous: string | undefined;
|
||||
switch (direction) {
|
||||
case "forward":
|
||||
previous = cursor ? runs.at(0)?.id : undefined;
|
||||
if (hasMore) {
|
||||
next = runs[PAGE_SIZE - 1]?.id;
|
||||
}
|
||||
previous = cursor ? runs.at(1)?.id : undefined;
|
||||
break;
|
||||
case "backward":
|
||||
if (hasMore) {
|
||||
next = runs[PAGE_SIZE - 1]?.id;
|
||||
previous = runs[1]?.id;
|
||||
}
|
||||
previous = runs.at(1)?.id;
|
||||
next = runs[PAGE_SIZE - 1]?.id;
|
||||
break;
|
||||
}
|
||||
|
||||
const runsToReturn =
|
||||
direction === "backward" && hasMore ? runs.slice(1, PAGE_SIZE + 1) : runs.slice(0, PAGE_SIZE);
|
||||
|
||||
return {
|
||||
runs: runs.slice(0, PAGE_SIZE).map((run) => ({
|
||||
runs: runsToReturn.map((run) => ({
|
||||
id: run.id,
|
||||
number: run.number,
|
||||
startedAt: run.startedAt,
|
||||
|
||||
@@ -13,10 +13,11 @@ type RunOptions = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type Run = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>;
|
||||
export type Task = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["tasks"][number];
|
||||
export type Event = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["event"];
|
||||
export type ViewRun = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>;
|
||||
export type ViewTask = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["tasks"][number];
|
||||
export type ViewEvent = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>["event"];
|
||||
|
||||
type QueryEvent = NonNullable<Awaited<ReturnType<RunPresenter["query"]>>>["event"];
|
||||
type QueryTask = NonNullable<Awaited<ReturnType<RunPresenter["query"]>>>["tasks"][number];
|
||||
|
||||
export class RunPresenter {
|
||||
@@ -76,7 +77,7 @@ export class RunPresenter {
|
||||
type: run.environment.type,
|
||||
slug: run.environment.slug,
|
||||
},
|
||||
event: run.event,
|
||||
event: this.#prepareEventData(run.event),
|
||||
tasks,
|
||||
runConnections: run.runConnections,
|
||||
missingConnections: run.missingConnections,
|
||||
@@ -84,6 +85,22 @@ export class RunPresenter {
|
||||
};
|
||||
}
|
||||
|
||||
#prepareEventData(event: QueryEvent) {
|
||||
return {
|
||||
id: event.eventId,
|
||||
name: event.name,
|
||||
payload: JSON.stringify(event.payload),
|
||||
context: JSON.stringify(event.context),
|
||||
timestamp: event.timestamp,
|
||||
deliveredAt: event.deliveredAt,
|
||||
externalAccount: event.externalAccount
|
||||
? {
|
||||
identifier: event.externalAccount.identifier,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
query({ id, userId }: RunOptions) {
|
||||
return this.#prismaClient.jobRun.findFirst({
|
||||
select: {
|
||||
@@ -110,9 +127,10 @@ export class RunPresenter {
|
||||
},
|
||||
event: {
|
||||
select: {
|
||||
id: true,
|
||||
eventId: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
context: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { RedactSchema } from "@trigger.dev/core";
|
||||
import { StyleSchema } from "@trigger.dev/core";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
import { Redactor } from "~/utils/redactor";
|
||||
|
||||
type DetailsProps = {
|
||||
id: string;
|
||||
@@ -61,6 +63,7 @@ export class TaskDetailsPresenter {
|
||||
completedAt: true,
|
||||
style: true,
|
||||
parentId: true,
|
||||
redact: true,
|
||||
attempts: {
|
||||
select: {
|
||||
number: true,
|
||||
@@ -85,11 +88,32 @@ export class TaskDetailsPresenter {
|
||||
|
||||
return {
|
||||
...task,
|
||||
output: task.output ? JSON.stringify(task.output, null, 2) : undefined,
|
||||
redact: undefined,
|
||||
output: task.output
|
||||
? JSON.stringify(this.#stringifyOutputWithRedactions(task.output, task.redact), null, 2)
|
||||
: undefined,
|
||||
connection: task.runConnection,
|
||||
params: task.params as Record<string, any>,
|
||||
properties: mergeProperties(task.properties, task.outputProperties),
|
||||
style: task.style ? StyleSchema.parse(task.style) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
#stringifyOutputWithRedactions(output: any, redact: unknown): any {
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedRedact = RedactSchema.safeParse(redact);
|
||||
|
||||
if (!parsedRedact.success) {
|
||||
return output;
|
||||
}
|
||||
|
||||
const paths = parsedRedact.data.paths;
|
||||
|
||||
const redactor = new Redactor(paths);
|
||||
|
||||
return redactor.redact(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Job } from "~/models/job.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { EventExample } from "@trigger.dev/core";
|
||||
|
||||
export class TestJobPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -67,14 +68,22 @@ export class TestJobPresenter {
|
||||
name: "latest",
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
runs: {
|
||||
select: {
|
||||
runs: {
|
||||
where: {
|
||||
isTest: true,
|
||||
id: true,
|
||||
createdAt: true,
|
||||
number: true,
|
||||
status: true,
|
||||
event: {
|
||||
select: {
|
||||
payload: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
@@ -97,6 +106,15 @@ export class TestJobPresenter {
|
||||
throw new Error("Job not found");
|
||||
}
|
||||
|
||||
//collect together the examples, we don't care about the environments
|
||||
const examples = job.aliases.flatMap((alias) =>
|
||||
alias.version.examples.map((example) => ({
|
||||
...example,
|
||||
icon: example.icon ?? undefined,
|
||||
payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
environments: job.aliases.map((alias) => ({
|
||||
id: alias.environment.id,
|
||||
@@ -104,15 +122,18 @@ export class TestJobPresenter {
|
||||
slug: alias.environment.slug,
|
||||
userId: alias.environment.orgMember?.userId,
|
||||
versionId: alias.version.id,
|
||||
examples: alias.version.examples.map((example) => ({
|
||||
...example,
|
||||
payload: JSON.stringify(example.payload, exampleReplacer, 2),
|
||||
})),
|
||||
hasAuthResolver: alias.version.integrations.some(
|
||||
(i) => i.integration.authSource === "RESOLVER"
|
||||
),
|
||||
})),
|
||||
hasTestRuns: job._count.runs > 0,
|
||||
examples,
|
||||
runs: job.runs.map((r) => ({
|
||||
id: r.id,
|
||||
number: r.number,
|
||||
status: r.status,
|
||||
created: r.createdAt,
|
||||
payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ export class TriggerDetailsPresenter {
|
||||
select: {
|
||||
event: {
|
||||
select: {
|
||||
id: true,
|
||||
eventId: true,
|
||||
name: true,
|
||||
payload: true,
|
||||
context: true,
|
||||
timestamp: true,
|
||||
deliveredAt: true,
|
||||
externalAccount: {
|
||||
@@ -32,6 +33,18 @@ export class TriggerDetailsPresenter {
|
||||
},
|
||||
});
|
||||
|
||||
return event;
|
||||
return {
|
||||
id: event.eventId,
|
||||
name: event.name,
|
||||
payload: JSON.stringify(event.payload, null, 2),
|
||||
context: JSON.stringify(event.context, null, 2),
|
||||
timestamp: event.timestamp,
|
||||
deliveredAt: event.deliveredAt,
|
||||
externalAccount: event.externalAccount
|
||||
? {
|
||||
identifier: event.externalAccount.identifier,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const links: LinksFunction = () => {
|
||||
export const meta: TypedMetaFunction<typeof loader> = ({ data }) => ({
|
||||
title: `Trigger.dev${appEnvTitleTag(data?.appEnv)}`,
|
||||
charset: "utf-8",
|
||||
viewport: "width=device-width,initial-scale=1",
|
||||
viewport: "width=1024, initial-scale=1",
|
||||
});
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
|
||||
+18
-5
@@ -19,6 +19,7 @@ import {
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Switch } from "~/components/primitives/Switch";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { useFilterJobs } from "~/hooks/useFilterJobs";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
@@ -62,8 +63,11 @@ export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const { jobs } = useTypedLoaderData<typeof loader>();
|
||||
const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs);
|
||||
const hasJobs = jobs.length > 0;
|
||||
const { filterText, setFilterText, filteredItems, onlyActiveJobs, setOnlyActiveJobs } =
|
||||
useFilterJobs(jobs);
|
||||
const totalJobs = jobs.length;
|
||||
const hasJobs = totalJobs > 0;
|
||||
const activeJobCount = jobs.filter((j) => j.status === "ACTIVE").length;
|
||||
|
||||
return (
|
||||
<PageContainer className={hasJobs ? "" : "grid-rows-1"}>
|
||||
@@ -74,7 +78,8 @@ export default function Page() {
|
||||
</PageTitleRow>
|
||||
<PageInfoRow>
|
||||
<PageInfoGroup>
|
||||
<PageInfoProperty icon={"job"} label={"Active Jobs"} value={jobs.length} />
|
||||
<PageInfoProperty icon={"job"} label={"All Jobs"} value={totalJobs} />
|
||||
<PageInfoProperty icon={"job"} label={"Active Jobs"} value={activeJobCount} />
|
||||
</PageInfoGroup>
|
||||
</PageInfoRow>
|
||||
</PageHeader>
|
||||
@@ -96,7 +101,7 @@ export default function Page() {
|
||||
</Callout>
|
||||
)}
|
||||
<div className="mb-2 flex flex-col">
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-full gap-x-2">
|
||||
<Input
|
||||
placeholder="Search Jobs"
|
||||
variant="tertiary"
|
||||
@@ -104,6 +109,14 @@ export default function Page() {
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Active Jobs"
|
||||
checked={onlyActiveJobs}
|
||||
onCheckedChange={setOnlyActiveJobs}
|
||||
className={"shrink-0"}
|
||||
/>
|
||||
<HelpTrigger title="Example Jobs and inspiration" />
|
||||
</div>
|
||||
@@ -160,7 +173,7 @@ function ExampleJobs() {
|
||||
height="250"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
className="mb-4 w-full border-b border-slate-800"
|
||||
className="mb-4 border-b border-slate-800"
|
||||
/>
|
||||
<Header2 spacing>How to create a Job</Header2>
|
||||
<Paragraph variant="small" spacing>
|
||||
|
||||
+49
-11
@@ -6,7 +6,7 @@ import { useEventSource } from "remix-utils";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Callout, CalloutVariant } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
@@ -18,8 +18,14 @@ import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader } from "~/components/primitives/Sheet";
|
||||
import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server";
|
||||
import { endpointStreamingPath } from "~/utils/pathBuilder";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { EndpointIndexStatus, RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
|
||||
import {
|
||||
EndpointIndexStatusIcon,
|
||||
EndpointIndexStatusLabel,
|
||||
endpointIndexStatusTitle,
|
||||
} from "~/components/environments/EndpointIndexStatus";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
|
||||
type ConfigureEndpointSheetProps = {
|
||||
slug: string;
|
||||
@@ -119,15 +125,29 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
method="post"
|
||||
action={`/resources/environments/${endpoint.environment.id}/endpoint/${endpoint.id}`}
|
||||
>
|
||||
<Callout variant="success" className="justiy-between items-center">
|
||||
<Paragraph variant="small" className="grow text-green-200">
|
||||
Endpoint configured. Last refreshed:{" "}
|
||||
{endpoint.latestIndex ? (
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
<Callout
|
||||
variant="info"
|
||||
icon={
|
||||
<EndpointIndexStatusIcon status={endpoint.latestIndex?.status ?? "PENDING"} />
|
||||
}
|
||||
className="justiy-between items-center"
|
||||
>
|
||||
<div className="flex grow items-center gap-2">
|
||||
<EndpointIndexStatusLabel
|
||||
status={endpoint.latestIndex?.status ?? "PENDING"}
|
||||
/>
|
||||
<Paragraph variant="small" className="grow">
|
||||
Last refreshed:{" "}
|
||||
{endpoint.latestIndex ? (
|
||||
<>
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
@@ -138,6 +158,11 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
{refreshingEndpoint ? "Refreshing" : "Refresh now"}
|
||||
</Button>
|
||||
</Callout>
|
||||
{endpoint.latestIndex?.error && (
|
||||
<FormError className="p-2">
|
||||
<pre>{endpoint.latestIndex.error.message}</pre>
|
||||
</FormError>
|
||||
)}
|
||||
</refreshEndpointFetcher.Form>
|
||||
</div>
|
||||
<div className="max-w-full overflow-hidden">
|
||||
@@ -155,3 +180,16 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function calloutVariantFromStatus(status: EndpointIndexStatus): CalloutVariant {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "pending";
|
||||
case "STARTED":
|
||||
return "pending";
|
||||
case "SUCCESS":
|
||||
return "success";
|
||||
case "FAILURE":
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
+34
-7
@@ -3,11 +3,16 @@ import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import {
|
||||
EndpointIndexStatusIcon,
|
||||
EndpointIndexStatusLabel,
|
||||
} from "~/components/environments/EndpointIndexStatus";
|
||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
@@ -38,7 +43,6 @@ import { ProjectParamSchema, projectEnvironmentsStreamingPath } from "~/utils/pa
|
||||
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) => {
|
||||
@@ -85,8 +89,8 @@ export default function Page() {
|
||||
const client = clients.find((c) => c.slug === selected.client);
|
||||
if (!client) return undefined;
|
||||
|
||||
if (selected.type === "PREVIEW" || selected.type === "STAGING") {
|
||||
throw new Error("PREVIEW/STAGING is not yet supported");
|
||||
if (selected.type === "PREVIEW") {
|
||||
throw new Error("PREVIEW is not yet supported");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -180,6 +184,7 @@ export default function Page() {
|
||||
<TableHeaderCell>Environment</TableHeaderCell>
|
||||
<TableHeaderCell>Url</TableHeaderCell>
|
||||
<TableHeaderCell>Last refreshed</TableHeaderCell>
|
||||
<TableHeaderCell>Last refresh Status</TableHeaderCell>
|
||||
<TableHeaderCell>Jobs</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -195,6 +200,18 @@ export default function Page() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
{client.endpoints.STAGING && (
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.STAGING}
|
||||
type="STAGING"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "STAGING",
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.PRODUCTION}
|
||||
type="PRODUCTION"
|
||||
@@ -218,7 +235,7 @@ export default function Page() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedEndpoint && (
|
||||
{selectedEndpoint && selectedEndpoint.endpoint && (
|
||||
<ConfigureEndpointSheet
|
||||
slug={selectedEndpoint.clientSlug}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
@@ -256,7 +273,7 @@ function EndpointRow({
|
||||
<EnvironmentLabel environment={{ type }} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick} colSpan={4} alignment="right">
|
||||
<TableCell onClick={onClick} colSpan={5} alignment="right">
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<span className="text-amber-500">
|
||||
The {environmentTitle({ type })} environment is not configured
|
||||
@@ -278,7 +295,17 @@ function EndpointRow({
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex ? <DateTime date={endpoint.latestIndex.updatedAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats.jobs ?? "–"}</TableCell>
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<EndpointIndexStatusIcon status={endpoint.latestIndex.status} />
|
||||
<EndpointIndexStatusLabel status={endpoint.latestIndex.status} />
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats?.jobs ?? "–"}</TableCell>
|
||||
<TableCellChevron onClick={onClick} />
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
+17
-73
@@ -13,6 +13,7 @@ import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { DetailCell } from "~/components/primitives/DetailCell";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
@@ -151,7 +152,7 @@ function PossibleIntegrationsList({
|
||||
onCheckedChange={setOnlyShowIntegrations}
|
||||
variant="small"
|
||||
label={
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="mt-0.5 inline-flex items-center gap-1">
|
||||
<IntegrationIcon /> Trigger.dev Integrations
|
||||
</span>
|
||||
}
|
||||
@@ -209,10 +210,12 @@ function PossibleIntegrationsList({
|
||||
<Feedback
|
||||
button={
|
||||
<button className="w-full">
|
||||
<ExternalIntegrationLink
|
||||
name="plus"
|
||||
<DetailCell
|
||||
leadingIcon="plus"
|
||||
leadingIconClassName="text-dimmed"
|
||||
label="Request an API and we'll add it to the list as an Integration"
|
||||
trailingIcon="chevron-right"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
@@ -221,10 +224,12 @@ function PossibleIntegrationsList({
|
||||
|
||||
<Header2 className="mb-2 mt-6">Create an Integration</Header2>
|
||||
<a href="https://docs.trigger.dev/integrations/create" target="_blank">
|
||||
<ExternalIntegrationLink
|
||||
name="integration"
|
||||
<DetailCell
|
||||
leadingIcon="integration"
|
||||
leadingIconClassName="text-dimmed"
|
||||
label="Learn how to create your own API Integrations"
|
||||
trailingIcon="external-link"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
@@ -482,77 +487,16 @@ function AddIntegrationConnection({
|
||||
icon?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-900">
|
||||
<NamedIconInBox
|
||||
name={icon ?? identifier}
|
||||
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
|
||||
/>
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className="m-0 flex-1 text-left leading-[1.1rem] transition group-hover:text-bright"
|
||||
>
|
||||
{name}
|
||||
</Paragraph>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
{isIntegration && <IntegrationIcon />}
|
||||
<NamedIcon
|
||||
name="plus"
|
||||
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExternalIntegrationLink({
|
||||
name,
|
||||
label,
|
||||
trailingIcon,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
trailingIcon: string;
|
||||
}) {
|
||||
return (
|
||||
<span className="group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-850">
|
||||
<NamedIconInBox
|
||||
name={name}
|
||||
className="h-9 w-9 flex-none text-dimmed transition group-hover:border-slate-750"
|
||||
iconClassName="text-dimmed"
|
||||
/>
|
||||
<Paragraph variant="base" className="m-0 flex-1 text-left transition group-hover:text-bright">
|
||||
{label}
|
||||
</Paragraph>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<NamedIcon
|
||||
name={trailingIcon}
|
||||
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<DetailCell
|
||||
className="w-full"
|
||||
leadingIcon={icon ?? identifier}
|
||||
label={name}
|
||||
trailingIcon="plus"
|
||||
trailingIconClassName="text-slate-700 group-hover:text-bright"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntegrationIcon() {
|
||||
return <LogoIcon className="h-3.5 w-3.5 flex-none pb-0.5" />;
|
||||
}
|
||||
|
||||
function InfoLink({ text }: { text: string }) {
|
||||
return (
|
||||
<div className="group flex h-11 w-full items-center gap-3 rounded-md p-1 pr-3 transition hover:bg-slate-850">
|
||||
<NamedIconInBox
|
||||
name="integration"
|
||||
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
|
||||
/>
|
||||
<Paragraph variant="base" className="m-0 flex-1 text-left transition group-hover:text-bright">
|
||||
{text}
|
||||
</Paragraph>
|
||||
<div className="flex flex-none items-center gap-1">
|
||||
<NamedIcon
|
||||
name="docs"
|
||||
className="h-6 w-6 flex-none text-slate-700 transition group-hover:text-bright"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-7
@@ -8,6 +8,7 @@ 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 { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { useFilterJobs } from "~/hooks/useFilterJobs";
|
||||
import { useIntegrationClient } from "~/hooks/useIntegrationClient";
|
||||
import { JobListPresenter } from "~/presenters/JobListPresenter.server";
|
||||
@@ -52,10 +53,8 @@ export default function Page() {
|
||||
{(open) => (
|
||||
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
|
||||
<div className="grow">
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
{jobs.length === 0 ? (
|
||||
<Header2>Jobs using this integration will appear here</Header2>
|
||||
) : (
|
||||
<div className="mb-2 flex items-center justify-end gap-x-2">
|
||||
{jobs.length !== 0 && (
|
||||
<Input
|
||||
placeholder="Search Jobs"
|
||||
variant="tertiary"
|
||||
@@ -68,9 +67,9 @@ export default function Page() {
|
||||
<HelpTrigger title="How do I use this integration?" />
|
||||
</div>
|
||||
{jobs.length === 0 ? (
|
||||
<>
|
||||
<JobSkeleton />
|
||||
</>
|
||||
<div className="mt-8 rounded border border-border px-2 py-6 text-center">
|
||||
<Paragraph variant="small">Jobs using this Integration will appear here.</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
<JobsTable
|
||||
jobs={filteredItems}
|
||||
|
||||
+196
-126
@@ -1,6 +1,7 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { PopoverTrigger } from "@radix-ui/react-popover";
|
||||
import { ClipboardIcon } from "@heroicons/react/20/solid";
|
||||
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
|
||||
import { Form, useActionData, useSubmit } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
@@ -8,16 +9,16 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { JSONEditor } from "~/components/code/JSONEditor";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { HowToRunATest } from "~/components/helpContent/HelpContentText";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { DetailCell } from "~/components/primitives/DetailCell";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Hint } from "~/components/primitives/Hint";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Popover, PopoverContent } from "~/components/primitives/Popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -26,12 +27,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses";
|
||||
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { TestJobPresenter } from "~/presenters/TestJobPresenter.server";
|
||||
import { TestJobService } from "~/services/jobs/testJob.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { isValidIcon } from "~/utils/icon";
|
||||
import { JobParamsSchema, jobRunDashboardPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
@@ -39,14 +43,14 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const { organizationSlug, projectParam, jobParam } = JobParamsSchema.parse(params);
|
||||
|
||||
const presenter = new TestJobPresenter();
|
||||
const { environments, hasTestRuns } = await presenter.call({
|
||||
const { environments, runs, examples } = await presenter.call({
|
||||
userId,
|
||||
organizationSlug,
|
||||
projectSlug: projectParam,
|
||||
jobSlug: jobParam,
|
||||
});
|
||||
|
||||
return typedjson({ environments, hasTestRuns });
|
||||
return typedjson({ environments, runs, examples });
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
@@ -116,22 +120,30 @@ export const handle: Handle = {
|
||||
const startingJson = "{\n\n}";
|
||||
|
||||
export default function Page() {
|
||||
const { environments, runs, examples } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
//form submission
|
||||
const submit = useSubmit();
|
||||
const lastSubmission = useActionData();
|
||||
const [isExamplePopoverOpen, setIsExamplePopoverOpen] = useState(false);
|
||||
const { environments, hasTestRuns } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
const [defaultJson, setDefaultJson] = useState<string>(startingJson);
|
||||
const currentJson = useRef<string>(defaultJson);
|
||||
//examples
|
||||
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(
|
||||
examples.at(0)?.id ?? runs.at(0)?.id
|
||||
);
|
||||
const selectedCodeSample =
|
||||
examples.find((e) => e.id === selectedCodeSampleId)?.payload ??
|
||||
runs.find((r) => r.id === selectedCodeSampleId)?.payload;
|
||||
|
||||
const [defaultJson, setDefaultJson] = useState<string>(selectedCodeSample ?? startingJson);
|
||||
const setCode = useCallback((code: string) => {
|
||||
setDefaultJson(code);
|
||||
}, []);
|
||||
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>(environments[0].id);
|
||||
const [currentAccountId, setCurrentAccountId] = useState<string | undefined>(undefined);
|
||||
|
||||
const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId);
|
||||
|
||||
const insertCode = useCallback((code: string) => {
|
||||
setDefaultJson(code);
|
||||
setIsExamplePopoverOpen(false);
|
||||
}, []);
|
||||
const currentJson = useRef<string>(defaultJson);
|
||||
const [currentAccountId, setCurrentAccountId] = useState<string | undefined>(undefined);
|
||||
|
||||
const submitForm = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -170,120 +182,178 @@ export default function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Help defaultOpen={true}>
|
||||
{(open) => (
|
||||
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
|
||||
<div className="flex h-fit max-h-full overflow-hidden">
|
||||
<Form
|
||||
className="flex max-h-full grow flex-col gap-2 overflow-y-auto"
|
||||
method="post"
|
||||
{...form.props}
|
||||
onSubmit={(e) => submitForm(e)}
|
||||
>
|
||||
<div className="flex flex-none items-center justify-between gap-2">
|
||||
<div className="flex flex-none items-center gap-2">
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={selectedEnvironmentId}
|
||||
onValueChange={setSelectedEnvironmentId}
|
||||
>
|
||||
<SelectTrigger size="secondary/small">
|
||||
<SelectValue placeholder="Select environment" className="m-0 p-0" />{" "}
|
||||
Environment
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{environments.map((environment) => (
|
||||
<SelectItem key={environment.id} value={environment.id}>
|
||||
<EnvironmentLabel environment={environment} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<div className="grid h-full grid-cols-1 gap-4">
|
||||
<div className="flex h-full max-h-full overflow-hidden">
|
||||
<Form
|
||||
className="flex h-full max-h-full grow flex-col gap-4 overflow-y-auto"
|
||||
method="post"
|
||||
{...form.props}
|
||||
onSubmit={(e) => submitForm(e)}
|
||||
>
|
||||
<div className="grid h-full grid-cols-[1fr_auto] overflow-hidden">
|
||||
<div className="relative h-full flex-1 overflow-hidden rounded-l border border-border">
|
||||
<JSONEditor
|
||||
defaultValue={defaultJson}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
onChange={(v) => {
|
||||
currentJson.current = v;
|
||||
|
||||
{selectedEnvironment && selectedEnvironment.examples.length > 0 && (
|
||||
<Popover
|
||||
open={isExamplePopoverOpen}
|
||||
onOpenChange={(open) => setIsExamplePopoverOpen(open)}
|
||||
//deselect the example if it's been edited
|
||||
if (selectedCodeSampleId) {
|
||||
if (v !== selectedCodeSample) {
|
||||
setDefaultJson(v);
|
||||
setSelectedCodeSampleId(undefined);
|
||||
}
|
||||
}
|
||||
}}
|
||||
height="100%"
|
||||
min-height="100%"
|
||||
max-height="100%"
|
||||
autoFocus
|
||||
placeholder="Use your schema to enter valid JSON or add one of the example payloads then click 'Run test'"
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full w-fit min-w-[20rem] flex-col gap-4 overflow-y-auto rounded-r border border-l-0 border-border p-4">
|
||||
{examples.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header2>Example payloads</Header2>
|
||||
{examples.map((example) => (
|
||||
<button
|
||||
type="button"
|
||||
key={example.id}
|
||||
onClick={(e) => {
|
||||
setCode(example.payload ?? "");
|
||||
setSelectedCodeSampleId(example.id);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<ButtonContent
|
||||
variant="secondary/small"
|
||||
LeadingIcon="beaker"
|
||||
TrailingIcon="chevron-down"
|
||||
>
|
||||
Insert an example
|
||||
</ButtonContent>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-80 p-0" align="start">
|
||||
{selectedEnvironment?.examples.map((example) => (
|
||||
<Button
|
||||
key={example.id}
|
||||
variant="menu-item"
|
||||
onClick={(e) => insertCode(example.payload)}
|
||||
LeadingIcon={example.icon ?? "beaker"}
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
{example.name}
|
||||
</Button>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
<DetailCell
|
||||
leadingIcon={isValidIcon(example.icon) ? example.icon : CodeBracketIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
label={example.name}
|
||||
trailingIcon={example.id === selectedCodeSampleId ? "check" : "plus"}
|
||||
trailingIconClassName={
|
||||
example.id === selectedCodeSampleId
|
||||
? "text-green-500 group-hover:text-green-400"
|
||||
: "text-slate-500 group-hover:text-bright"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<HelpTrigger title="How do I run a test?" />
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header2>Recent payloads</Header2>
|
||||
{runs.length === 0 ? (
|
||||
<Callout variant="info">
|
||||
Recent payloads will show here once you've completed a Run.
|
||||
</Callout>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{runs.map((run) => (
|
||||
<button
|
||||
key={run.id}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
setCode(run.payload ?? "");
|
||||
setSelectedCodeSampleId(run.id);
|
||||
}}
|
||||
>
|
||||
<DetailCell
|
||||
leadingIcon={ClockIcon}
|
||||
leadingIconClassName="text-slate-400"
|
||||
label={<DateTime date={run.created} />}
|
||||
description={
|
||||
<>
|
||||
Run #{run.number}{" "}
|
||||
<span className={runStatusClassNameColor(run.status)}>
|
||||
{runStatusTitle(run.status).toLocaleLowerCase()}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
trailingIcon={run.id === selectedCodeSampleId ? "check" : "plus"}
|
||||
trailingIconClassName={
|
||||
run.id === selectedCodeSampleId
|
||||
? "text-green-500 group-hover:text-green-400"
|
||||
: "text-slate-500 group-hover:text-bright"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Payload</Label>
|
||||
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<JSONEditor
|
||||
defaultValue={defaultJson}
|
||||
readOnly={false}
|
||||
basicSetup
|
||||
onChange={(v) => (currentJson.current = v)}
|
||||
minHeight="150px"
|
||||
/>
|
||||
</div>
|
||||
</InputGroup>
|
||||
|
||||
{selectedEnvironment?.hasAuthResolver && (
|
||||
<InputGroup fullWidth className="mb-4 mt-4">
|
||||
<Label variant="small">Account ID</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
value={currentAccountId}
|
||||
placeholder={`e.g. abc_1234`}
|
||||
onChange={(e) => setCurrentAccountId(e.target.value)}
|
||||
/>
|
||||
<FormError>{accountId.error}</FormError>
|
||||
</InputGroup>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Header2>Account ID</Header2>
|
||||
<InputGroup fullWidth>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="large"
|
||||
value={currentAccountId}
|
||||
placeholder={`e.g. abc_1234`}
|
||||
onChange={(e) => setCurrentAccountId(e.target.value)}
|
||||
/>
|
||||
<FormError>{accountId.error}</FormError>
|
||||
<Hint>
|
||||
Learn about testing Jobs with an Account ID in our{" "}
|
||||
<TextLink href="https://trigger.dev/docs/documentation/guides/using-integrations-byo-auth#testing-jobs-with-account-id">
|
||||
BYOAuth docs
|
||||
</TextLink>
|
||||
</Hint>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-none items-center justify-between">
|
||||
{payload.error ? (
|
||||
<FormError id={payload.errorId}>{payload.error}</FormError>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
LeadingIcon="beaker"
|
||||
leadingIconClassName="text-bright"
|
||||
>
|
||||
Run test
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
<HelpContent title="How to run a test" className="h-fit">
|
||||
<HowToRunATest />
|
||||
</HelpContent>
|
||||
</div>
|
||||
)}
|
||||
</Help>
|
||||
<div className="flex items-center justify-between">
|
||||
<LinkButton
|
||||
variant="tertiary/medium"
|
||||
to="https://trigger.dev/docs/documentation/guides/testing-jobs"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Learn more about running tests
|
||||
</LinkButton>
|
||||
<div className="flex flex-none items-center justify-end gap-2">
|
||||
{payload.error ? (
|
||||
<FormError id={payload.errorId}>{payload.error}</FormError>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={selectedEnvironmentId}
|
||||
onValueChange={setSelectedEnvironmentId}
|
||||
>
|
||||
<SelectTrigger size="medium">
|
||||
<SelectValue placeholder="Select environment" className="m-0 p-0" /> Environment
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{environments.map((environment) => (
|
||||
<SelectItem key={environment.id} value={environment.id}>
|
||||
<EnvironmentLabel environment={environment} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
LeadingIcon="beaker"
|
||||
leadingIconClassName="text-bright"
|
||||
shortcut={{ key: "enter", modifiers: ["mod"], enabledOnInputElements: true }}
|
||||
>
|
||||
Run test
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
@@ -44,6 +44,9 @@ export default function SetUpAstro() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<AstroLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
|
||||
+108
-9
@@ -1,21 +1,120 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Express" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="Express"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/451"
|
||||
githubIssueNumber={451}
|
||||
>
|
||||
<ExpressLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<ExpressLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Callout
|
||||
variant={"info"}
|
||||
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
|
||||
className="mb-8"
|
||||
>
|
||||
Trigger.dev has full support for serverless. We will be adding support for long-running
|
||||
servers soon.
|
||||
</Callout>
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Manually set up Trigger.dev in your existing Express project"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/express"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your Express app" />
|
||||
<StepContentContainer>
|
||||
<RunDevCommand />
|
||||
<Callout variant="info">
|
||||
You may be using the `start` script instead, in which case substitute `dev` in the
|
||||
above commands.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
+211
-12
@@ -1,21 +1,220 @@
|
||||
import { NestjsLogo } from "~/assets/logos/NestjsLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { InstallPackages } from "~/components/code/InstallPackages";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { CodeBlock } from "../../components/code/CodeBlock";
|
||||
import { TriggerDevStep } from "~/components/SetupCommands";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Nest.js" />,
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="NestJS" />,
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const AppModuleCode = `
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TriggerDevModule } from '@trigger.dev/nestjs';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TriggerDevModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
id: 'my-nest-app',
|
||||
apiKey: config.getOrThrow('TRIGGER_API_KEY'),
|
||||
apiUrl: config.getOrThrow('TRIGGER_API_URL'),
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
`;
|
||||
|
||||
const JobControllerCode = `
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { InjectTriggerDevClient } from '@trigger.dev/nestjs';
|
||||
import { eventTrigger, TriggerClient } from '@trigger.dev/sdk';
|
||||
|
||||
@Controller()
|
||||
export class JobController {
|
||||
constructor(
|
||||
@InjectTriggerDevClient() private readonly client: TriggerClient,
|
||||
) {
|
||||
this.client.defineJob({
|
||||
id: 'test-job',
|
||||
name: 'Test Job One',
|
||||
version: '0.0.1',
|
||||
trigger: eventTrigger({
|
||||
name: 'test.event',
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.logger.info('Hello world!', { payload });
|
||||
|
||||
return {
|
||||
message: 'Hello world!',
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return \`Running Trigger.dev with client-id \${this.client.id}\`;
|
||||
}
|
||||
}`;
|
||||
|
||||
const AppModuleWithControllerCode = `
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TriggerDevModule } from '@trigger.dev/nestjs';
|
||||
import { JobController } from './job.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
TriggerDevModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
id: 'my-nest-app',
|
||||
apiKey: config.getOrThrow('TRIGGER_API_KEY'),
|
||||
apiUrl: config.getOrThrow('TRIGGER_API_URL'),
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [
|
||||
//...existingControllers,
|
||||
JobController
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
`;
|
||||
|
||||
const packageJsonCode = `"trigger.dev": {
|
||||
"endpointId": "my-nest-app"
|
||||
}`;
|
||||
|
||||
export default function SetupNestJS() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
invariant(devEnvironment, "devEnvironment is required");
|
||||
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="Nest.js"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/449"
|
||||
githubIssueNumber={449}
|
||||
>
|
||||
<NestjsLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 2 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Add the dependencies" />
|
||||
<StepContentContainer>
|
||||
<InstallPackages
|
||||
packages={["@trigger.dev/sdk", "@trigger.dev/nestjs", "@nestjs/config"]}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Add the environment variables" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Inside your <InlineCode>.env</InlineCode> file, create the following env variables:
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
fileName=".env"
|
||||
showChrome
|
||||
code={`TRIGGER_API_KEY=${devEnvironment.apiKey}\nTRIGGER_API_URL=${appOrigin}`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Add the TriggerDevModule" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Now, go to your <InlineCode>app.module.ts</InlineCode> and add the{" "}
|
||||
<InlineCode>TriggerDevModule</InlineCode>:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="app.module.ts" showChrome code={AppModuleCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Add the first job" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Create a <InlineCode>controller</InlineCode> called{" "}
|
||||
<InlineCode>job.controller.ts</InlineCode> and add the following code:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="src/job.controller.ts" showChrome code={JobControllerCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="5" title="Update your app.module.ts" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Now, add the new <InlineCode>controller</InlineCode> to your{" "}
|
||||
<InlineCode>app.module.ts</InlineCode>:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="app.module.ts" showChrome code={AppModuleWithControllerCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Update your package.json" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Now, add this to the top-level of your <InlineCode>package.json</InlineCode>:
|
||||
</Paragraph>
|
||||
<CodeBlock fileName="package.json" showChrome code={packageJsonCode} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="7" title="Run your app" />
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph>
|
||||
Finally, run your project with <InlineCode>npm run start</InlineCode>:
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="8" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="9" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
+4
@@ -28,6 +28,7 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
|
||||
|
||||
type SelectionChoices = "use-existing-project" | "create-new-next-app";
|
||||
|
||||
@@ -48,6 +49,9 @@ export default function SetupNextjs() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<NextjsLogo className="w-56" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes
|
||||
|
||||
+4
@@ -27,6 +27,7 @@ import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { RemixLogo } from "~/assets/logos/RemixLogo";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="Remix" />,
|
||||
@@ -43,6 +44,9 @@ export default function SetUpRemix() {
|
||||
return (
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
<RemixLogo className="w-64" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
|
||||
+102
-12
@@ -1,23 +1,113 @@
|
||||
import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => (
|
||||
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="SvelteKit" />
|
||||
),
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
export default function SetUpSveltekit() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="SvelteKit"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/453"
|
||||
githubIssueNumber={453}
|
||||
>
|
||||
<SvelteKitLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Callout
|
||||
variant={"info"}
|
||||
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
|
||||
className="mb-8"
|
||||
>
|
||||
Trigger.dev has full support for serverless. We will be adding support for long-running
|
||||
servers soon.
|
||||
</Callout>
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Follow the steps from the Sveltekit manual installation guide"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/sveltekit"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
<div className="flex items-start justify-start gap-2"></div>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your sveltekit app" />
|
||||
<StepContentContainer>
|
||||
<RunDevCommand extra=" -- --open --host" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep extra=" --port 5173" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ export default function App() {
|
||||
return (
|
||||
<>
|
||||
{impersonationId && <ImpersonationBanner impersonationId={impersonationId} />}
|
||||
<NoMobileOverlay />
|
||||
<AppContainer showBackgroundGradient={showBackgroundGradient}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
@@ -72,7 +71,6 @@ export default function App() {
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<>
|
||||
<NoMobileOverlay />
|
||||
<AppContainer showBackgroundGradient={true}>
|
||||
<MainCenteredContainer>
|
||||
<RouteErrorDisplay />
|
||||
|
||||
@@ -30,89 +30,80 @@ export async function action({ request }: ActionArgs) {
|
||||
});
|
||||
}
|
||||
|
||||
const headerClassName =
|
||||
"py-3 px-2 pr-3 text-xs font-semibold leading-tight text-slate-900 text-left";
|
||||
const cellClassName = "whitespace-nowrap px-2 py-2 text-xs text-slate-500";
|
||||
const headerClassName = "py-3 px-2 pr-3 text-xs font-semibold leading-tight text-bright text-left";
|
||||
const cellClassName = "whitespace-nowrap px-2 py-2 text-xs text-bright";
|
||||
|
||||
export default function AdminDashboardRoute() {
|
||||
const { users } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 overflow-hidden">
|
||||
{/* Primary column */}
|
||||
<section
|
||||
aria-labelledby="primary-heading"
|
||||
className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto p-4 lg:order-last"
|
||||
>
|
||||
<h1 className="mb-2 text-2xl">Accounts ({users.length})</h1>
|
||||
<main
|
||||
aria-labelledby="primary-heading"
|
||||
className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto p-4 lg:order-last"
|
||||
>
|
||||
<h1 className="mb-2 text-2xl">Accounts ({users.length})</h1>
|
||||
|
||||
<table className="w-full divide-y divide-slate-300">
|
||||
<thead className="sticky top-0 bg-white text-left outline outline-2 outline-slate-200">
|
||||
<tr>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Email
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
GitHub username
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
id
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Created At
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Admin?
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200 bg-white">
|
||||
{users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id} className="w-full bg-white px-4 py-2 text-left hover:bg-slate-50">
|
||||
<td className={cellClassName}>{user.email}</td>
|
||||
<td className={cellClassName}>
|
||||
<a
|
||||
href={`https://github.com/${user.displayName}`}
|
||||
target="_blank"
|
||||
className="text-indigo-500 underline"
|
||||
rel="noreferrer"
|
||||
<table className="w-full divide-y divide-border">
|
||||
<thead className="sticky -top-4 bg-midnight-800 text-left">
|
||||
<tr>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Email
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
GitHub username
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
id
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Created At
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Admin?
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id} className="w-full px-4 py-2 text-left hover:bg-slate-900">
|
||||
<td className={cellClassName}>{user.email}</td>
|
||||
<td className={cellClassName}>
|
||||
<a
|
||||
href={`https://github.com/${user.displayName}`}
|
||||
target="_blank"
|
||||
className="text-indigo-500 underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{user.displayName}
|
||||
</a>
|
||||
</td>
|
||||
<td className={cellClassName}>{user.id}</td>
|
||||
<td className={cellClassName}>{user.createdAt.toISOString()}</td>
|
||||
<td className={cellClassName}>{user.admin ? "✅" : ""}</td>
|
||||
<td className={cellClassName}>
|
||||
<Form method="post" reloadDocument>
|
||||
<input type="hidden" name="id" value={user.id} />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
name="action"
|
||||
value="impersonate"
|
||||
className="mr-2"
|
||||
variant="primary/small"
|
||||
>
|
||||
{user.displayName}
|
||||
</a>
|
||||
</td>
|
||||
<td className={cellClassName}>{user.id}</td>
|
||||
<td className={cellClassName}>{user.createdAt.toISOString()}</td>
|
||||
<td className={cellClassName}>{user.admin ? "✅" : ""}</td>
|
||||
<td className={cellClassName}>
|
||||
<Form method="post" reloadDocument>
|
||||
<input type="hidden" name="id" value={user.id} />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
name="action"
|
||||
value="impersonate"
|
||||
className="mr-2"
|
||||
variant="primary/small"
|
||||
>
|
||||
Impersonate
|
||||
</Button>
|
||||
</Form>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
{/* Secondary column (hidden on smaller screens) */}
|
||||
<aside className="hidden lg:order-first lg:block lg:flex-shrink-0">
|
||||
<div className="relative flex h-full w-96 flex-col overflow-y-auto border-r border-gray-200 bg-white"></div>
|
||||
</aside>
|
||||
Impersonate
|
||||
</Button>
|
||||
</Form>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ActionArgs, json, redirect } from "@remix-run/server-runtime";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const adminOrgMembers = await prisma.orgMember.findMany({
|
||||
where: {
|
||||
organizationId: authenticationResult.environment.organizationId,
|
||||
user: {
|
||||
admin: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!adminOrgMembers.length) {
|
||||
return json({ error: "You must be an admin to perform this action" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body: any = await request.json();
|
||||
|
||||
await workerQueue.enqueue("simulate", {
|
||||
seconds: body.seconds,
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { HomeIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { UserCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { HomeIcon } from "@heroicons/react/24/outline";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment, useState } from "react";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import type { User } from "~/models/user.server";
|
||||
import { getUser, requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const navigation = [{ name: "Home", href: "/admin", icon: HomeIcon }];
|
||||
|
||||
export async function loader({ request }: LoaderArgs) {
|
||||
await requireUserId(request);
|
||||
@@ -27,162 +20,10 @@ export async function loader({ request }: LoaderArgs) {
|
||||
|
||||
export default function Page() {
|
||||
const data = useTypedLoaderData<typeof loader>();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full">
|
||||
<Transition.Root show={mobileMenuOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-40 lg:hidden" onClose={setMobileMenuOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white focus:outline-none">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute top-0 right-0 -mr-12 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
<div className="pt-5 pb-4">
|
||||
<div className="flex flex-shrink-0 items-center px-4">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600"
|
||||
alt="Workflow"
|
||||
/>
|
||||
</div>
|
||||
<nav aria-label="Sidebar" className="mt-5">
|
||||
<div className="space-y-1 px-2">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="group flex items-center rounded-md p-2 text-base font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||
>
|
||||
<item.icon
|
||||
className="mr-4 h-6 w-6 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 border-t border-gray-200 p-4">
|
||||
<button className="group block flex-shrink-0">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<UserProfilePhoto user={data.user} className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-base font-medium text-gray-700 group-hover:text-gray-900">
|
||||
{data.user.displayName}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-500 group-hover:text-gray-700">
|
||||
Account Settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<div className="w-14 flex-shrink-0" aria-hidden="true">
|
||||
{/* Force sidebar to shrink to fit close icon */}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="flex w-20 flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-indigo-600">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-center bg-indigo-700 py-4">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://tailwindui.com/img/logos/workflow-mark.svg?color=white"
|
||||
alt="Workflow"
|
||||
/>
|
||||
</div>
|
||||
<nav aria-label="Sidebar" className="flex flex-col items-center space-y-3 py-6">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="flex items-center rounded-lg p-4 text-indigo-200 hover:bg-indigo-700"
|
||||
>
|
||||
<item.icon className="h-6 w-6" aria-hidden="true" />
|
||||
<span className="sr-only">{item.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 pb-5">
|
||||
<button className="flex w-full flex-shrink-0 flex-grow justify-center">
|
||||
<UserProfilePhoto user={data.user} className="block h-10 w-10" />
|
||||
<div className="sr-only">
|
||||
<p>{data.user.displayName}</p>
|
||||
<p>Account settings</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UserProfilePhoto({ user, className }: { user: User; className?: string }) {
|
||||
return user.avatarUrl ? (
|
||||
<img
|
||||
className={cn("rounded-full", className)}
|
||||
src={user.avatarUrl}
|
||||
alt={user.name ?? user.displayName ?? "User"}
|
||||
/>
|
||||
) : (
|
||||
<UserCircleIcon className={cn("text-gray-400", className)} />
|
||||
<div className="h-full w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ActionArgs, json } from "@remix-run/server-runtime";
|
||||
import {
|
||||
EndpointIndexErrorSchema,
|
||||
GetEndpointIndexResponse,
|
||||
GetEndpointIndexResponseSchema,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
indexId: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ request, params }: ActionArgs) {
|
||||
if (request.method.toUpperCase() !== "GET") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const parsedParams = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
return json({ error: "Invalid params" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
if (!authenticationResult) {
|
||||
logger.info("Invalid or missing api key", { url: request.url });
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const authenticatedEnv = authenticationResult.environment;
|
||||
|
||||
const { indexId } = parsedParams.data;
|
||||
|
||||
const endpointIndex = await prisma.endpointIndex.findUnique({
|
||||
where: {
|
||||
id: indexId,
|
||||
endpoint: {
|
||||
environmentId: authenticatedEnv.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!endpointIndex) {
|
||||
logger.info("EndpointIndex not found", { url: request.url });
|
||||
return json({ error: "EndpointIndex not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const parsed = GetEndpointIndexResponseSchema.safeParse(endpointIndex);
|
||||
|
||||
if (!parsed.success) {
|
||||
logger.info("EndpointIndex failed parsing", { errors: parsed.error.issues, endpointIndex });
|
||||
const parseFailResult: GetEndpointIndexResponse = {
|
||||
status: "FAILURE",
|
||||
error: {
|
||||
message: "Invalid endpoint index",
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
return json(parseFailResult, { status: 500 });
|
||||
}
|
||||
|
||||
return json(parsed.data);
|
||||
}
|
||||
+45
-35
@@ -1,7 +1,7 @@
|
||||
import { ActionArgs, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { safeJsonParse } from "~/utils/json";
|
||||
@@ -93,43 +93,53 @@ export class TriggerEndpointIndexHookService {
|
||||
body,
|
||||
});
|
||||
|
||||
const endpoint = await this.#prismaClient.endpoint.findUnique({
|
||||
where: {
|
||||
environmentId_slug: {
|
||||
environmentId,
|
||||
slug: endpointSlug,
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
const endpoint = await tx.endpoint.findUnique({
|
||||
where: {
|
||||
environmentId_slug: {
|
||||
environmentId,
|
||||
slug: endpointSlug,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error("Endpoint not found");
|
||||
}
|
||||
|
||||
if (endpoint.indexingHookIdentifier !== indexHookIdentifier) {
|
||||
throw new Error("Index hook identifier is invalid");
|
||||
}
|
||||
|
||||
const reason = parseReasonFromBody(body);
|
||||
|
||||
// Index the endpoint in 5 seconds from now
|
||||
await workerQueue.enqueue(
|
||||
"indexEndpoint",
|
||||
{
|
||||
id: endpoint.id,
|
||||
source: "HOOK",
|
||||
reason,
|
||||
sourceData: body,
|
||||
},
|
||||
{
|
||||
runAt: new Date(Date.now() + 5000),
|
||||
maxAttempts:
|
||||
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
|
||||
if (!endpoint) {
|
||||
throw new Error("Endpoint not found");
|
||||
}
|
||||
);
|
||||
|
||||
if (endpoint.indexingHookIdentifier !== indexHookIdentifier) {
|
||||
throw new Error("Index hook identifier is invalid");
|
||||
}
|
||||
|
||||
const reason = parseReasonFromBody(body);
|
||||
|
||||
const index = await tx.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
status: "PENDING",
|
||||
source: "HOOK",
|
||||
reason,
|
||||
sourceData: body,
|
||||
},
|
||||
});
|
||||
|
||||
// Index the endpoint in 5 seconds from now
|
||||
await workerQueue.enqueue(
|
||||
"performEndpointIndexing",
|
||||
{
|
||||
id: index.id,
|
||||
},
|
||||
{
|
||||
runAt: new Date(Date.now() + 5000),
|
||||
maxAttempts:
|
||||
endpoint.environment.type === RuntimeEnvironmentType.DEVELOPMENT ? 1 : undefined,
|
||||
tx,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { PrismaErrorSchema } from "~/db.server";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { CancelRunService } from "~/services/runs/cancelRun.server";
|
||||
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
// Authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const parsed = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsed.success) {
|
||||
return json({ error: "Invalid or Missing runId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { runId } = parsed.data;
|
||||
|
||||
const service = new CancelRunService();
|
||||
try {
|
||||
await service.call({ runId });
|
||||
} catch (error) {
|
||||
const prismaError = PrismaErrorSchema.safeParse(error);
|
||||
// Record not found in the database
|
||||
if (prismaError.success && prismaError.data.code === "P2005") {
|
||||
return json({ error: "Run not found" }, { status: 404 });
|
||||
} else {
|
||||
return json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
const presenter = new ApiRunPresenter();
|
||||
const jobRun = await presenter.call({
|
||||
runId: runId,
|
||||
});
|
||||
|
||||
if (!jobRun) {
|
||||
return json({ message: "Run not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({
|
||||
id: jobRun.id,
|
||||
status: jobRun.status,
|
||||
startedAt: jobRun.startedAt,
|
||||
updatedAt: jobRun.updatedAt,
|
||||
completedAt: jobRun.completedAt,
|
||||
output: jobRun.output,
|
||||
tasks: jobRun.tasks,
|
||||
statuses: jobRun.statuses.map((s) => ({
|
||||
...s,
|
||||
state: s.state ?? undefined,
|
||||
data: s.data ?? undefined,
|
||||
history: s.history ?? undefined,
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { TaskStatus } from "@trigger.dev/database";
|
||||
import {
|
||||
RunTaskBodyOutput,
|
||||
RunTaskBodyOutputSchema,
|
||||
ServerTask,
|
||||
JobRunStatusRecordSchema,
|
||||
StatusHistory,
|
||||
StatusHistorySchema,
|
||||
StatusUpdate,
|
||||
@@ -14,12 +11,8 @@ import {
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { ulid } from "~/services/ulid.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
id: z.string(),
|
||||
secret: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const { runId, id } = ParamsSchema.parse(params);
|
||||
|
||||
// Parse body as JSON (no schema parsing)
|
||||
const body = await request.json();
|
||||
|
||||
const service = new CallbackRunTaskService();
|
||||
|
||||
try {
|
||||
// Complete task with request body as output
|
||||
await service.call(runId, id, body, request.url);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Error while processing task callback:", { error });
|
||||
}
|
||||
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export class CallbackRunTaskService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(runId: string, id: string, taskBody: any, callbackUrl: string): Promise<void> {
|
||||
const task = await findTask(prisma, id);
|
||||
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.runId !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.status !== "WAITING") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!task.callbackUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new URL(task.callbackUrl).pathname !== new URL(callbackUrl).pathname) {
|
||||
logger.error("Callback URLs don't match", { runId, taskId: id, callbackUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("CallbackRunTaskService.call()", { task });
|
||||
|
||||
await this.#resumeTask(task, taskBody);
|
||||
}
|
||||
|
||||
async #resumeTask(task: NonNullable<FoundTask>, output: any) {
|
||||
await $transaction(this.#prismaClient, async (tx) => {
|
||||
await tx.taskAttempt.updateMany({
|
||||
where: {
|
||||
taskId: task.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
completedAt: new Date(),
|
||||
output: output ? output : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await this.#resumeRunExecution(task, tx);
|
||||
});
|
||||
}
|
||||
|
||||
async #resumeRunExecution(task: NonNullable<FoundTask>, prisma: PrismaClientOrTransaction) {
|
||||
await enqueueRunExecutionV2(task.run, prisma, {
|
||||
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type FoundTask = Awaited<ReturnType<typeof findTask>>;
|
||||
|
||||
async function findTask(prisma: PrismaClientOrTransaction, id: string) {
|
||||
return prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
run: {
|
||||
include: {
|
||||
environment: true,
|
||||
queue: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
|
||||
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
|
||||
import { CompleteTaskBodyInputSchema } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -72,9 +72,9 @@ export async function action({ request, params }: ActionArgs) {
|
||||
}
|
||||
|
||||
export class CompleteRunTaskService {
|
||||
#prismaClient: PrismaClient;
|
||||
#prismaClient: PrismaClientOrTransaction;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ export class CompleteRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// Using a transaction, we'll first check to see if the task already exists and return if if it does
|
||||
// If it doesn't exist, we'll create it and return it
|
||||
const task = await this.#prismaClient.$transaction(async (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await $transaction(this.#prismaClient, async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,32 @@ export class CompleteRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
run: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -86,8 +86,8 @@ export class FailRunTaskService {
|
||||
): Promise<ServerTask | undefined> {
|
||||
// Using a transaction, we'll first check to see if the task already exists and return if if it does
|
||||
// If it doesn't exist, we'll create it and return it
|
||||
const task = await this.#prismaClient.$transaction(async (prisma) => {
|
||||
const existingTask = await prisma.task.findUnique({
|
||||
const task = await this.#prismaClient.$transaction(async (tx) => {
|
||||
const existingTask = await tx.task.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -129,35 +129,32 @@ export class FailRunTaskService {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
const task = await $transaction(prisma, async (tx) => {
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.task.update({
|
||||
if (existingTask.attempts.length === 1) {
|
||||
await tx.taskAttempt.update({
|
||||
where: {
|
||||
id,
|
||||
id: existingTask.attempts[0].id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
error: formatError(taskBody.error),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return task;
|
||||
return await tx.task.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "ERRORED",
|
||||
output: taskBody.error ?? undefined,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
run: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { TaskStatus } from "@trigger.dev/database";
|
||||
import { RunTaskBodyOutput, RunTaskBodyOutputSchema, ServerTask } from "@trigger.dev/core";
|
||||
import {
|
||||
API_VERSIONS,
|
||||
RunTaskBodyOutput,
|
||||
RunTaskBodyOutputSchema,
|
||||
RunTaskResponseWithCachedTasksBody,
|
||||
ServerTask,
|
||||
} from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import { $transaction, PrismaClient, prisma } from "~/db.server";
|
||||
import { taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import { prepareTasksForCaching, taskWithAttemptsToServerTask } from "~/models/task.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { ulid } from "~/services/ulid.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { generateSecret } from "~/services/sources/utils.server";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
@@ -16,6 +24,8 @@ const ParamsSchema = z.object({
|
||||
|
||||
const HeadersSchema = z.object({
|
||||
"idempotency-key": z.string(),
|
||||
"trigger-version": z.string().optional().nullable(),
|
||||
"x-cached-tasks-cursor": z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
@@ -37,7 +47,11 @@ export async function action({ request, params }: ActionArgs) {
|
||||
return json({ error: "Invalid or Missing idempotency key" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { "idempotency-key": idempotencyKey } = headers.data;
|
||||
const {
|
||||
"idempotency-key": idempotencyKey,
|
||||
"trigger-version": triggerVersion,
|
||||
"x-cached-tasks-cursor": cachedTasksCursor,
|
||||
} = headers.data;
|
||||
|
||||
const { runId } = ParamsSchema.parse(params);
|
||||
|
||||
@@ -48,6 +62,8 @@ export async function action({ request, params }: ActionArgs) {
|
||||
body: anyBody,
|
||||
runId,
|
||||
idempotencyKey,
|
||||
triggerVersion,
|
||||
cachedTasksCursor,
|
||||
});
|
||||
|
||||
const body = RunTaskBodyOutputSchema.safeParse(anyBody);
|
||||
@@ -71,6 +87,26 @@ export async function action({ request, params }: ActionArgs) {
|
||||
return json({ error: "Something went wrong" }, { status: 500 });
|
||||
}
|
||||
|
||||
if (triggerVersion === API_VERSIONS.LAZY_LOADED_CACHED_TASKS) {
|
||||
const requestMigration = new ChangeRequestLazyLoadedCachedTasks();
|
||||
|
||||
const responseBody = await requestMigration.call(runId, task, cachedTasksCursor);
|
||||
|
||||
logger.debug(
|
||||
"RunTaskService.call() response migrating with ChangeRequestLazyLoadedCachedTasks",
|
||||
{
|
||||
responseBody,
|
||||
cachedTasksCursor,
|
||||
}
|
||||
);
|
||||
|
||||
return json(responseBody, {
|
||||
headers: {
|
||||
"trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return json(task);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -81,6 +117,51 @@ export async function action({ request, params }: ActionArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
class ChangeRequestLazyLoadedCachedTasks {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(
|
||||
runId: string,
|
||||
task: ServerTask,
|
||||
cursor?: string | null
|
||||
): Promise<RunTaskResponseWithCachedTasksBody> {
|
||||
if (!cursor) {
|
||||
return {
|
||||
task,
|
||||
};
|
||||
}
|
||||
|
||||
// We need to limit the cached tasks to not be too large >2MB when serialized
|
||||
const TOTAL_CACHED_TASK_BYTE_LIMIT = 2000000;
|
||||
|
||||
const nextTasks = await this.#prismaClient.task.findMany({
|
||||
where: {
|
||||
runId,
|
||||
status: "COMPLETED",
|
||||
noop: false,
|
||||
},
|
||||
take: 250,
|
||||
cursor: {
|
||||
id: cursor,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
const preparedTasks = prepareTasksForCaching(nextTasks, TOTAL_CACHED_TASK_BYTE_LIMIT);
|
||||
|
||||
return {
|
||||
task,
|
||||
cachedTasks: preparedTasks,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class RunTaskService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
@@ -103,13 +184,17 @@ export class RunTaskService {
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
run: true,
|
||||
},
|
||||
});
|
||||
|
||||
const delayUntilInFuture = taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now();
|
||||
const callbackEnabled = taskBody.callback?.enabled;
|
||||
|
||||
if (existingTask) {
|
||||
if (existingTask.status === "CANCELED") {
|
||||
const existingTaskStatus =
|
||||
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
|
||||
delayUntilInFuture || callbackEnabled || taskBody.trigger
|
||||
? "WAITING"
|
||||
: taskBody.noop
|
||||
? "COMPLETED"
|
||||
@@ -154,16 +239,21 @@ export class RunTaskService {
|
||||
status = "CANCELED";
|
||||
} else {
|
||||
status =
|
||||
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
|
||||
delayUntilInFuture || callbackEnabled || taskBody.trigger
|
||||
? "WAITING"
|
||||
: taskBody.noop
|
||||
? "COMPLETED"
|
||||
: "RUNNING";
|
||||
}
|
||||
|
||||
const taskId = ulid();
|
||||
const callbackUrl = callbackEnabled
|
||||
? `${env.APP_ORIGIN}/api/v1/runs/${runId}/tasks/${taskId}/callback/${generateSecret(12)}`
|
||||
: undefined;
|
||||
|
||||
const task = await tx.task.create({
|
||||
data: {
|
||||
id: ulid(),
|
||||
id: taskId,
|
||||
idempotencyKey,
|
||||
displayKey: taskBody.displayKey,
|
||||
runConnection: taskBody.connectionKey
|
||||
@@ -191,9 +281,10 @@ export class RunTaskService {
|
||||
noop: taskBody.noop,
|
||||
delayUntil: taskBody.delayUntil,
|
||||
params: taskBody.params ?? undefined,
|
||||
properties: taskBody.properties ?? undefined,
|
||||
properties: this.#filterProperties(taskBody.properties) ?? undefined,
|
||||
redact: taskBody.redact ?? undefined,
|
||||
operation: taskBody.operation,
|
||||
callbackUrl,
|
||||
style: taskBody.style ?? { style: "normal" },
|
||||
attempts: {
|
||||
create: {
|
||||
@@ -215,8 +306,19 @@ export class RunTaskService {
|
||||
{
|
||||
id: task.id,
|
||||
},
|
||||
{ tx, runAt: task.delayUntil ?? undefined }
|
||||
{ tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` }
|
||||
);
|
||||
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
|
||||
if (taskBody.callback.timeoutInSeconds > 0) {
|
||||
// We need to schedule the callback timeout
|
||||
await workerQueue.enqueue(
|
||||
"processCallbackTimeout",
|
||||
{
|
||||
id: task.id,
|
||||
},
|
||||
{ tx, runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
@@ -224,4 +326,14 @@ export class RunTaskService {
|
||||
|
||||
return task ? taskWithAttemptsToServerTask(task) : undefined;
|
||||
}
|
||||
|
||||
#filterProperties(properties: RunTaskBodyOutput["properties"]): RunTaskBodyOutput["properties"] {
|
||||
if (!properties) return;
|
||||
|
||||
return properties.filter((property) => {
|
||||
if (!property) return false;
|
||||
|
||||
return typeof property.label === "string" && typeof property.text === "string";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
import { taskListToTree } from "~/utils/taskListToTree";
|
||||
@@ -51,51 +51,15 @@ export async function loader({ request, params }: LoaderArgs) {
|
||||
|
||||
const query = parsedQuery.data;
|
||||
const showTaskDetails = query.taskdetails && authenticationResult.type === "PRIVATE";
|
||||
|
||||
const take = Math.min(query.take, 50);
|
||||
|
||||
const jobRun = await prisma.jobRun.findUnique({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
startedAt: true,
|
||||
updatedAt: true,
|
||||
completedAt: true,
|
||||
environmentId: true,
|
||||
output: true,
|
||||
tasks: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
displayKey: true,
|
||||
status: true,
|
||||
name: true,
|
||||
icon: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
params: showTaskDetails,
|
||||
output: showTaskDetails,
|
||||
},
|
||||
where: {
|
||||
parentId: query.subtasks ? undefined : null,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
take: take + 1,
|
||||
cursor: query.cursor
|
||||
? {
|
||||
id: query.cursor,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
statuses: {
|
||||
select: { key: true, label: true, state: true, data: true, history: true },
|
||||
},
|
||||
},
|
||||
const presenter = new ApiRunPresenter();
|
||||
const jobRun = await presenter.call({
|
||||
runId: runId,
|
||||
maxTasks: take,
|
||||
taskDetails: showTaskDetails,
|
||||
subTasks: query.subtasks,
|
||||
cursor: query.cursor,
|
||||
});
|
||||
|
||||
if (!jobRun) {
|
||||
|
||||
@@ -6,11 +6,24 @@ import { HandleHttpSourceService } from "~/services/sources/handleHttpSource.ser
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
logger.info("Handling http source", { url: request.url });
|
||||
|
||||
const { id } = z.object({ id: z.string() }).parse(params);
|
||||
try {
|
||||
const { id } = z.object({ id: z.string() }).parse(params);
|
||||
const service = new HandleHttpSourceService();
|
||||
const result = await service.call(id, request);
|
||||
|
||||
const service = new HandleHttpSourceService();
|
||||
|
||||
return await service.call(id, request);
|
||||
return new Response(undefined, {
|
||||
status: result.status,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
logger.error("Error handling http source", { error: e.message });
|
||||
} else {
|
||||
logger.error("Error handling http source", { error: JSON.stringify(e) });
|
||||
}
|
||||
return new Response(undefined, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loader({ request, params }: LoaderArgs) {
|
||||
|
||||
@@ -60,16 +60,29 @@ export default function LoginPage() {
|
||||
<a href="https://trigger.dev">
|
||||
<LogoIcon className="mb-4 h-16 w-16" />
|
||||
</a>
|
||||
<FormTitle divide={false} title="Log in to Trigger.dev" />
|
||||
<FormTitle divide={false} title="Welcome to Trigger.dev" className="mb-2 pb-0" />
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Create an account or login
|
||||
</Paragraph>
|
||||
<Fieldset>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{data.showGithubAuth && (
|
||||
<Button type="submit" variant="primary/large" fullWidth>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/large"
|
||||
fullWidth
|
||||
data-action="continue with github"
|
||||
>
|
||||
<NamedIcon name={"github"} className={"mr-1.5 h-4 w-4"} />
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
)}
|
||||
<LinkButton to="/login/magic" variant="secondary/large" fullWidth>
|
||||
<LinkButton
|
||||
to="/login/magic"
|
||||
variant="secondary/large"
|
||||
fullWidth
|
||||
data-action="continue with email"
|
||||
>
|
||||
<NamedIcon
|
||||
name={"envelope"}
|
||||
className={"mr-1.5 h-4 w-4 text-dimmed transition group-hover:text-bright"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ActionArgs, LoaderArgs } from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { Form, useTransition } from "@remix-run/react";
|
||||
import { Form, useNavigation, useTransition } from "@remix-run/react";
|
||||
import { TypedMetaFunction, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { LogoIcon } from "~/components/LogoIcon";
|
||||
@@ -17,10 +17,10 @@ import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { commitSession, getUserSession } from "~/services/sessionStorage.server";
|
||||
import magicLinkIcon from "./login.magic.svg";
|
||||
|
||||
import type { LoaderType as RootLoader } from "~/root";
|
||||
import { appEnvTitleTag } from "~/utils";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
|
||||
export const meta: TypedMetaFunction<typeof loader, { root: RootLoader }> = ({ parentsData }) => ({
|
||||
title: `Login to Trigger.dev${appEnvTitleTag(parentsData?.root.appEnv)}`,
|
||||
@@ -32,10 +32,26 @@ export async function loader({ request }: LoaderArgs) {
|
||||
});
|
||||
|
||||
const session = await getUserSession(request);
|
||||
const error = session.get("auth:error");
|
||||
|
||||
return typedjson({
|
||||
magicLinkSent: session.has("triggerdotdev:magiclink"),
|
||||
});
|
||||
let magicLinkError: string | undefined;
|
||||
if (error) {
|
||||
if ("message" in error) {
|
||||
magicLinkError = error.message;
|
||||
} else {
|
||||
magicLinkError = JSON.stringify(error, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return typedjson(
|
||||
{
|
||||
magicLinkSent: session.has("triggerdotdev:magiclink"),
|
||||
magicLinkError,
|
||||
},
|
||||
{
|
||||
headers: { "Set-Cookie": await commitSession(session) },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
@@ -50,7 +66,7 @@ export async function action({ request }: ActionArgs) {
|
||||
.parse(payload);
|
||||
|
||||
if (action === "send") {
|
||||
await authenticator.authenticate("email-link", request, {
|
||||
return authenticator.authenticate("email-link", request, {
|
||||
successRedirect: "/login/magic",
|
||||
failureRedirect: "/login/magic",
|
||||
});
|
||||
@@ -67,13 +83,13 @@ export async function action({ request }: ActionArgs) {
|
||||
}
|
||||
|
||||
export default function LoginMagicLinkPage() {
|
||||
const { magicLinkSent } = useTypedLoaderData<typeof loader>();
|
||||
const transition = useTransition();
|
||||
const { magicLinkSent, magicLinkError } = useTypedLoaderData<typeof loader>();
|
||||
const navigate = useNavigation();
|
||||
|
||||
const isLoading =
|
||||
(transition.state === "loading" || transition.state === "submitting") &&
|
||||
transition.type === "actionSubmission" &&
|
||||
transition.submission.formData.get("action") === "send";
|
||||
(navigate.state === "loading" || navigate.state === "submitting") &&
|
||||
navigate.formAction !== undefined &&
|
||||
navigate.formData?.get("action") === "send";
|
||||
|
||||
return (
|
||||
<AppContainer showBackgroundGradient={true}>
|
||||
@@ -102,12 +118,17 @@ export default function LoginMagicLinkPage() {
|
||||
variant="tertiary/small"
|
||||
LeadingIcon="arrow-left"
|
||||
leadingIconClassName="text-dimmed group-hover:text-bright transition"
|
||||
data-action="re-enter email"
|
||||
>
|
||||
Re-enter email
|
||||
</Button>
|
||||
}
|
||||
confirmButton={
|
||||
<LinkButton to="/login" variant="tertiary/small">
|
||||
<LinkButton
|
||||
to="/login"
|
||||
variant="tertiary/small"
|
||||
data-action="log in using another option"
|
||||
>
|
||||
Log in using another option
|
||||
</LinkButton>
|
||||
}
|
||||
@@ -116,7 +137,10 @@ export default function LoginMagicLinkPage() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormTitle divide={false} title="Log in to Trigger.dev" />
|
||||
<FormTitle divide={false} title="Welcome to Trigger.dev" className="mb-2 pb-0" />
|
||||
<Paragraph variant="small" className="mb-4 text-center">
|
||||
Create an account or login using your email
|
||||
</Paragraph>
|
||||
<Fieldset className="flex w-full flex-col items-center gap-y-2">
|
||||
<InputGroup>
|
||||
<Label>Your email address</Label>
|
||||
@@ -137,6 +161,7 @@ export default function LoginMagicLinkPage() {
|
||||
variant="primary/medium"
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
data-action="send a magic link"
|
||||
>
|
||||
<NamedIcon
|
||||
name={isLoading ? "spinner-white" : "envelope"}
|
||||
@@ -144,6 +169,7 @@ export default function LoginMagicLinkPage() {
|
||||
/>
|
||||
{isLoading ? "Sending…" : "Send a magic link"}
|
||||
</Button>
|
||||
{magicLinkError && <FormError>{magicLinkError}</FormError>}
|
||||
</Fieldset>
|
||||
<Paragraph variant="extra-small" className="my-4 text-center">
|
||||
By logging in with your email you agree to our{" "}
|
||||
@@ -162,11 +188,28 @@ export default function LoginMagicLinkPage() {
|
||||
variant={"tertiary/small"}
|
||||
LeadingIcon={"arrow-left"}
|
||||
leadingIconClassName="text-dimmed group-hover:text-bright transition"
|
||||
data-action="all login options"
|
||||
>
|
||||
All login options
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-8 rounded border border-border px-6 py-4">
|
||||
<Paragraph variant="small" className="mb-2 text-center">
|
||||
Having login issues?
|
||||
</Paragraph>
|
||||
<Paragraph variant="extra-small" className="text-center">
|
||||
Ensure the Magic Link email isn't in your spam folder. If the problem persists,{" "}
|
||||
<TextLink href="mailto:help@trigger.dev" target="_blank">
|
||||
drop us an email
|
||||
</TextLink>{" "}
|
||||
or let us know on{" "}
|
||||
<TextLink href="https://trigger.dev/discord" target="_blank">
|
||||
Discord
|
||||
</TextLink>
|
||||
.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</MainCenteredContainer>
|
||||
|
||||
+11
-11
@@ -1,26 +1,26 @@
|
||||
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 { IndexEndpointService } from "~/services/endpoints/indexEndpoint.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
environmentParam: z.string(),
|
||||
endpointParam: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { environmentParam, endpointParam } = ParamsSchema.parse(params);
|
||||
export async function action({ params }: ActionArgs) {
|
||||
const { endpointParam } = ParamsSchema.parse(params);
|
||||
|
||||
try {
|
||||
const service = new IndexEndpointService();
|
||||
const result = await service.call(endpointParam, "MANUAL");
|
||||
await service.call(endpointParam, "MANUAL");
|
||||
|
||||
// Enqueue the endpoint to be probed in 10 seconds
|
||||
await workerQueue.enqueue(
|
||||
"probeEndpoint",
|
||||
{ id: endpointParam },
|
||||
{ jobKey: `probe:${endpointParam}`, runAt: new Date(Date.now() + 10000) }
|
||||
);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,28 +2,15 @@ import { parse } from "@conform-to/zod";
|
||||
import { ActionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import {
|
||||
CreateEndpointError,
|
||||
CreateEndpointService,
|
||||
} from "~/services/endpoints/createEndpoint.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { RuntimeEnvironmentTypeSchema } from "@trigger.dev/core";
|
||||
import { env } from "process";
|
||||
import { CreateEndpointError } from "~/services/endpoints/createEndpoint.server";
|
||||
import { ValidateCreateEndpointService } from "~/services/endpoints/validateCreateEndpoint.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectId: z.string(),
|
||||
});
|
||||
|
||||
export const bodySchema = z.object({
|
||||
environmentId: z.string(),
|
||||
url: z.string().url("Must be a valid URL"),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectId } = ParamsSchema.parse(params);
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema: bodySchema });
|
||||
|
||||
@@ -48,7 +35,7 @@ export async function action({ request, params }: ActionArgs) {
|
||||
}
|
||||
|
||||
const service = new ValidateCreateEndpointService();
|
||||
const result = await service.call({
|
||||
await service.call({
|
||||
url: submission.value.url,
|
||||
environment,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DeliverEmail } from "emails";
|
||||
import type { DeliverEmail, SendPlainTextOptions } from "emails";
|
||||
import { EmailClient } from "emails";
|
||||
import type { SendEmailOptions } from "remix-auth-email-link";
|
||||
import { redirect } from "remix-typedjson";
|
||||
@@ -6,6 +6,7 @@ import { env } from "~/env.server";
|
||||
import type { User } from "~/models/user.server";
|
||||
import type { AuthUser } from "./authUser";
|
||||
import { workerQueue } from "./worker.server";
|
||||
import { logger } from "./logger.server";
|
||||
|
||||
const client = new EmailClient({
|
||||
apikey: env.RESEND_API_KEY,
|
||||
@@ -20,11 +21,22 @@ export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): P
|
||||
throw redirect(options.magicLink);
|
||||
}
|
||||
|
||||
return client.send({
|
||||
email: "magic_link",
|
||||
to: options.emailAddress,
|
||||
magicLink: options.magicLink,
|
||||
});
|
||||
logger.debug("Sending magic link email", { emailAddress: options.emailAddress });
|
||||
|
||||
try {
|
||||
return await client.send({
|
||||
email: "magic_link",
|
||||
to: options.emailAddress,
|
||||
magicLink: options.magicLink,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error sending magic link email", { error: JSON.stringify(error) });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendPlainTextEmail(options: SendPlainTextOptions) {
|
||||
return client.sendPlainText(options);
|
||||
}
|
||||
|
||||
export async function scheduleWelcomeEmail(user: User) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { findOrCreateUser } from "~/models/user.server";
|
||||
import { env } from "~/env.server";
|
||||
import { sendMagicLinkEmail } from "~/services/email.server";
|
||||
import { postAuthentication } from "./postAuth.server";
|
||||
import { logger } from "./logger.server";
|
||||
|
||||
let secret = env.MAGIC_LINK_SECRET;
|
||||
if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable.");
|
||||
@@ -25,6 +26,8 @@ const emailStrategy = new EmailLinkStrategy(
|
||||
form: FormData;
|
||||
magicLinkVerify: boolean;
|
||||
}) => {
|
||||
logger.info("Magic link user authenticated", { email, magicLinkVerify });
|
||||
|
||||
try {
|
||||
const { user, isNewUser } = await findOrCreateUser({
|
||||
email,
|
||||
@@ -35,6 +38,7 @@ const emailStrategy = new EmailLinkStrategy(
|
||||
|
||||
return { userId: user.id };
|
||||
} catch (error) {
|
||||
logger.debug("Magic link user failed to authenticate", { error: JSON.stringify(error) });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
API_VERSIONS,
|
||||
ApiEventLog,
|
||||
DeliverEventResponseSchema,
|
||||
DeserializedJson,
|
||||
EndpointHeadersSchema,
|
||||
ErrorWithStackSchema,
|
||||
HttpSourceRequest,
|
||||
HttpSourceResponseSchema,
|
||||
@@ -89,10 +91,21 @@ export class EndpointApi {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (headers.success && headers.data["trigger-version"]) {
|
||||
return {
|
||||
...pongResponse.data,
|
||||
triggerVersion: headers.data["trigger-version"],
|
||||
triggerSdkVersion: headers.data["trigger-sdk-version"],
|
||||
};
|
||||
}
|
||||
|
||||
return pongResponse.data;
|
||||
}
|
||||
|
||||
async indexEndpoint() {
|
||||
const startTimeInMs = performance.now();
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -102,66 +115,13 @@ export class EndpointApi {
|
||||
},
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new 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) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`);
|
||||
}
|
||||
|
||||
const anyBody = await response.json();
|
||||
|
||||
const data = IndexEndpointResponseSchema.parse(anyBody);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
async deliverEvent(event: ApiEventLog) {
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-trigger-api-key": this.apiKey,
|
||||
"x-trigger-action": "DELIVER_EVENT",
|
||||
},
|
||||
body: JSON.stringify(event),
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not connect to endpoint ${this.url}. Status code: ${response.status}`);
|
||||
}
|
||||
|
||||
const anyBody = await response.json();
|
||||
|
||||
logger.debug("deliverEvent() response from endpoint", {
|
||||
body: anyBody,
|
||||
});
|
||||
|
||||
return DeliverEventResponseSchema.parse(anyBody);
|
||||
response,
|
||||
headerParser: EndpointHeadersSchema,
|
||||
parser: IndexEndpointResponseSchema,
|
||||
errorParser: ErrorWithStackSchema,
|
||||
durationInMs: Math.floor(performance.now() - startTimeInMs),
|
||||
};
|
||||
}
|
||||
|
||||
async executeJobRequest(options: RunJobBody) {
|
||||
@@ -338,8 +298,38 @@ export class EndpointApi {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = EndpointHeadersSchema.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (headers.success && headers.data["trigger-version"]) {
|
||||
return {
|
||||
...validateResponse.data,
|
||||
triggerVersion: headers.data["trigger-version"],
|
||||
};
|
||||
}
|
||||
|
||||
return validateResponse.data;
|
||||
}
|
||||
|
||||
async probe(timeout: number) {
|
||||
const startTimeInMs = performance.now();
|
||||
|
||||
const response = await safeFetch(this.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-api-key": this.apiKey,
|
||||
"x-trigger-action": "PROBE_EXECUTION_TIMEOUT",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
timeout,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
response,
|
||||
durationInMs: Math.floor(performance.now() - startTimeInMs),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function safeFetch(url: string, options: RequestInit) {
|
||||
@@ -359,6 +349,7 @@ function addStandardRequestOptions(options: RequestInit) {
|
||||
headers: {
|
||||
...options.headers,
|
||||
"user-agent": "triggerdotdev-server/2.0.0",
|
||||
"x-trigger-version": API_VERSIONS.LAZY_LOADED_CACHED_TASKS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,18 +74,27 @@ export class CreateEndpointService {
|
||||
slug: id,
|
||||
url: endpointUrl,
|
||||
indexingHookIdentifier: indexingHookIdentifier(),
|
||||
version: pong.triggerVersion,
|
||||
},
|
||||
update: {
|
||||
url: endpointUrl,
|
||||
version: pong.triggerVersion,
|
||||
},
|
||||
});
|
||||
|
||||
const endpointIndex = await tx.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
status: "PENDING",
|
||||
source: "INTERNAL",
|
||||
},
|
||||
});
|
||||
|
||||
// Kick off process to fetch the jobs for this endpoint
|
||||
await workerQueue.enqueue(
|
||||
"indexEndpoint",
|
||||
"performEndpointIndexing",
|
||||
{
|
||||
id: endpoint.id,
|
||||
source: "INTERNAL",
|
||||
id: endpointIndex.id,
|
||||
},
|
||||
{
|
||||
tx,
|
||||
@@ -94,7 +103,7 @@ export class CreateEndpointService {
|
||||
}
|
||||
);
|
||||
|
||||
return endpoint;
|
||||
return { ...endpoint, endpointIndex };
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import type { EndpointIndexSource } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { findEndpoint } from "~/models/endpoint.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { RegisterJobService } from "../jobs/registerJob.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { RegisterSourceServiceV1 } from "../sources/registerSourceV1.server";
|
||||
import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
|
||||
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
|
||||
import { DisableJobService } from "../jobs/disableJob.server";
|
||||
import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server";
|
||||
import { PerformEndpointIndexService } from "./performEndpointIndexService";
|
||||
|
||||
export class IndexEndpointService {
|
||||
#prismaClient: PrismaClient;
|
||||
#registerJobService = new RegisterJobService();
|
||||
#disableJobService = new DisableJobService();
|
||||
#registerSourceServiceV1 = new RegisterSourceServiceV1();
|
||||
#registerSourceServiceV2 = new RegisterSourceServiceV2();
|
||||
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
|
||||
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
@@ -29,207 +15,17 @@ export class IndexEndpointService {
|
||||
reason?: string,
|
||||
sourceData?: any
|
||||
) {
|
||||
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);
|
||||
|
||||
const indexResponse = await client.indexEndpoint();
|
||||
|
||||
if (!indexResponse.ok) {
|
||||
throw new Error(indexResponse.error);
|
||||
}
|
||||
|
||||
const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data;
|
||||
|
||||
logger.debug("Indexing endpoint", {
|
||||
endpointId: endpoint.id,
|
||||
endpointUrl: endpoint.url,
|
||||
endpointSlug: endpoint.slug,
|
||||
source: source,
|
||||
sourceData: sourceData,
|
||||
stats: {
|
||||
jobs: jobs.length,
|
||||
sources: sources.length,
|
||||
dynamicTriggers: dynamicTriggers.length,
|
||||
dynamicSchedules: dynamicSchedules.length,
|
||||
},
|
||||
});
|
||||
|
||||
const indexStats = {
|
||||
jobs: 0,
|
||||
sources: 0,
|
||||
dynamicTriggers: 0,
|
||||
dynamicSchedules: 0,
|
||||
disabledJobs: 0,
|
||||
};
|
||||
|
||||
const existingJobs = await this.#prismaClient.job.findMany({
|
||||
where: {
|
||||
projectId: endpoint.projectId,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
aliases: {
|
||||
where: {
|
||||
name: "latest",
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
include: {
|
||||
version: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const job of jobs) {
|
||||
if (!job.enabled) {
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, { slug: job.id, version: job.version })
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const registeredVersion = await this.#registerJobService.call(endpoint, job);
|
||||
|
||||
if (registeredVersion) {
|
||||
indexStats.jobs++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to register job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we need to do this for sources, dynamic triggers, and dynamic schedules
|
||||
const missingJobs = existingJobs.filter((job) => {
|
||||
return !jobs.find((j) => j.id === job.slug);
|
||||
});
|
||||
|
||||
if (missingJobs.length > 0) {
|
||||
logger.debug("Disabling missing jobs", {
|
||||
endpointId: endpoint.id,
|
||||
missingJobIds: missingJobs.map((job) => job.slug),
|
||||
});
|
||||
|
||||
for (const job of missingJobs) {
|
||||
const latestVersion = job.aliases[0]?.version;
|
||||
|
||||
if (!latestVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, {
|
||||
slug: job.slug,
|
||||
version: latestVersion.version,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
switch (source.version) {
|
||||
default:
|
||||
case "1": {
|
||||
await this.#registerSourceServiceV1.call(endpoint, source);
|
||||
break;
|
||||
}
|
||||
case "2": {
|
||||
await this.#registerSourceServiceV2.call(endpoint, source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
indexStats.sources++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register source", {
|
||||
endpointId: endpoint.id,
|
||||
source,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicTrigger of dynamicTriggers) {
|
||||
try {
|
||||
await this.#registerDynamicTriggerService.call(endpoint, dynamicTrigger);
|
||||
|
||||
indexStats.dynamicTriggers++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic trigger", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicTrigger,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicSchedule of dynamicSchedules) {
|
||||
try {
|
||||
await this.#registerDynamicScheduleService.call(endpoint, dynamicSchedule);
|
||||
|
||||
indexStats.dynamicSchedules++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic schedule", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicSchedule,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Endpoint indexing complete", {
|
||||
endpointId: endpoint.id,
|
||||
indexStats,
|
||||
source,
|
||||
sourceData,
|
||||
reason,
|
||||
});
|
||||
|
||||
return await this.#prismaClient.endpointIndex.create({
|
||||
const endpointIndex = await this.#prismaClient.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
stats: indexStats,
|
||||
data: {
|
||||
jobs,
|
||||
sources,
|
||||
dynamicTriggers,
|
||||
dynamicSchedules,
|
||||
},
|
||||
endpointId: id,
|
||||
status: "PENDING",
|
||||
source,
|
||||
sourceData,
|
||||
reason,
|
||||
sourceData,
|
||||
},
|
||||
});
|
||||
|
||||
const performEndpointIndexService = new PerformEndpointIndexService();
|
||||
return await performEndpointIndexService.call(endpointIndex.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import type { EndpointIndexSource } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { findEndpoint } from "~/models/endpoint.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { RegisterJobService } from "../jobs/registerJob.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { RegisterSourceServiceV1 } from "../sources/registerSourceV1.server";
|
||||
import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
|
||||
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
|
||||
import { DisableJobService } from "../jobs/disableJob.server";
|
||||
import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server";
|
||||
import { EndpointIndexError } from "@trigger.dev/core";
|
||||
import { safeBodyFromResponse } from "~/utils/json";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { IndexEndpointStats } from "@trigger.dev/core";
|
||||
|
||||
export class PerformEndpointIndexService {
|
||||
#prismaClient: PrismaClient;
|
||||
#registerJobService = new RegisterJobService();
|
||||
#disableJobService = new DisableJobService();
|
||||
#registerSourceServiceV1 = new RegisterSourceServiceV1();
|
||||
#registerSourceServiceV2 = new RegisterSourceServiceV2();
|
||||
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
|
||||
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(id: string) {
|
||||
const endpointIndex = await this.#prismaClient.endpointIndex.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "STARTED",
|
||||
},
|
||||
include: {
|
||||
endpoint: {
|
||||
include: {
|
||||
environment: {
|
||||
include: {
|
||||
organization: true,
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug("Performing endpoint index", endpointIndex);
|
||||
|
||||
// Make a request to the endpoint to fetch a list of jobs
|
||||
const client = new EndpointApi(
|
||||
endpointIndex.endpoint.environment.apiKey,
|
||||
endpointIndex.endpoint.url
|
||||
);
|
||||
const { response, parser, headerParser, errorParser } = await client.indexEndpoint();
|
||||
|
||||
if (!response) {
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: `Could not connect to endpoint ${endpointIndex.endpoint.url}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
const body = await safeBodyFromResponse(response, errorParser);
|
||||
|
||||
if (body) {
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: body.message,
|
||||
});
|
||||
}
|
||||
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: "Trigger API key is invalid",
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: `Could not connect to endpoint ${endpointIndex.endpoint.url}. Status code: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
const anyBody = await response.json();
|
||||
const bodyResult = parser.safeParse(anyBody);
|
||||
|
||||
if (!bodyResult.success) {
|
||||
const issues: string[] = [];
|
||||
bodyResult.error.issues.forEach((issue) => {
|
||||
if (issue.path.at(0) === "jobs") {
|
||||
const jobIndex = issue.path.at(1) as number;
|
||||
const job = (anyBody as any).jobs[jobIndex];
|
||||
|
||||
if (job) {
|
||||
issues.push(`Job "${job.id}": ${issue.message} at "${issue.path.slice(2).join(".")}".`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let friendlyError: string | undefined;
|
||||
if (issues.length > 0) {
|
||||
friendlyError = `Your Jobs have issues:\n${issues.map((issue) => `- ${issue}`).join("\n")}`;
|
||||
} else {
|
||||
friendlyError = fromZodError(bodyResult.error, {
|
||||
prefix: "There's an issue with the format of your Jobs",
|
||||
}).message;
|
||||
}
|
||||
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: friendlyError,
|
||||
raw: bodyResult.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
const headerResult = headerParser.safeParse(Object.fromEntries(response.headers.entries()));
|
||||
if (!headerResult.success) {
|
||||
const friendlyError = fromZodError(headerResult.error, {
|
||||
prefix: "Your headers are invalid",
|
||||
});
|
||||
return updateEndpointIndexWithError(this.#prismaClient, id, {
|
||||
message: friendlyError.message,
|
||||
raw: headerResult.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
const { jobs, sources, dynamicTriggers, dynamicSchedules } = bodyResult.data;
|
||||
const { "trigger-version": triggerVersion, "trigger-sdk-version": triggerSdkVersion } =
|
||||
headerResult.data;
|
||||
const { endpoint } = endpointIndex;
|
||||
|
||||
if (
|
||||
(triggerVersion && triggerVersion !== endpoint.version) ||
|
||||
(triggerSdkVersion && triggerSdkVersion !== endpoint.sdkVersion)
|
||||
) {
|
||||
await this.#prismaClient.endpoint.update({
|
||||
where: {
|
||||
id: endpoint.id,
|
||||
},
|
||||
data: {
|
||||
version: triggerVersion,
|
||||
sdkVersion: triggerSdkVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const indexStats: IndexEndpointStats = {
|
||||
jobs: 0,
|
||||
sources: 0,
|
||||
dynamicTriggers: 0,
|
||||
dynamicSchedules: 0,
|
||||
disabledJobs: 0,
|
||||
};
|
||||
|
||||
const existingJobs = await this.#prismaClient.job.findMany({
|
||||
where: {
|
||||
projectId: endpoint.projectId,
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
aliases: {
|
||||
where: {
|
||||
name: "latest",
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
include: {
|
||||
version: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const job of jobs) {
|
||||
if (!job.enabled) {
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, { slug: job.id, version: job.version })
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const registeredVersion = await this.#registerJobService.call(endpoint, job);
|
||||
|
||||
if (registeredVersion) {
|
||||
if (!job.internal) {
|
||||
indexStats.jobs++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to register job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we need to do this for sources, dynamic triggers, and dynamic schedules
|
||||
const missingJobs = existingJobs.filter((job) => {
|
||||
return !jobs.find((j) => j.id === job.slug);
|
||||
});
|
||||
|
||||
if (missingJobs.length > 0) {
|
||||
logger.debug("Disabling missing jobs", {
|
||||
endpointId: endpoint.id,
|
||||
missingJobIds: missingJobs.map((job) => job.slug),
|
||||
});
|
||||
|
||||
for (const job of missingJobs) {
|
||||
const latestVersion = job.aliases[0]?.version;
|
||||
|
||||
if (!latestVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const disabledJob = await this.#disableJobService
|
||||
.call(endpoint, {
|
||||
slug: job.slug,
|
||||
version: latestVersion.version,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Failed to disable job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
error,
|
||||
});
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
if (disabledJob) {
|
||||
indexStats.disabledJobs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
switch (source.version) {
|
||||
default:
|
||||
case "1": {
|
||||
await this.#registerSourceServiceV1.call(endpoint, source);
|
||||
break;
|
||||
}
|
||||
case "2": {
|
||||
await this.#registerSourceServiceV2.call(endpoint, source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
indexStats.sources++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register source", {
|
||||
endpointId: endpoint.id,
|
||||
source,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicTrigger of dynamicTriggers) {
|
||||
try {
|
||||
await this.#registerDynamicTriggerService.call(endpoint, dynamicTrigger);
|
||||
|
||||
indexStats.dynamicTriggers++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic trigger", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicTrigger,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicSchedule of dynamicSchedules) {
|
||||
try {
|
||||
await this.#registerDynamicScheduleService.call(endpoint, dynamicSchedule);
|
||||
|
||||
indexStats.dynamicSchedules++;
|
||||
} catch (error) {
|
||||
logger.error("Failed to register dynamic schedule", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicSchedule,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Endpoint indexing complete", {
|
||||
endpointId: endpoint.id,
|
||||
indexStats,
|
||||
source: endpointIndex.source,
|
||||
sourceData: endpointIndex.sourceData,
|
||||
reason: endpointIndex.reason,
|
||||
});
|
||||
|
||||
return await this.#prismaClient.endpointIndex.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "SUCCESS",
|
||||
stats: indexStats,
|
||||
data: {
|
||||
jobs,
|
||||
sources,
|
||||
dynamicTriggers,
|
||||
dynamicSchedules,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateEndpointIndexWithError(
|
||||
prismaClient: PrismaClient,
|
||||
id: string,
|
||||
error: EndpointIndexError
|
||||
) {
|
||||
return await prismaClient.endpointIndex.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "FAILURE",
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user