Compare commits
371 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17a117d1ee | |||
| bc43cc232d | |||
| 94ae9e65f4 | |||
| 2ed7158cf1 | |||
| 9d8a21a3de | |||
| d313eaa742 | |||
| c5d91e7cca | |||
| cc668a1624 | |||
| d17173ec0a | |||
| ebcd886d20 | |||
| 4976bd1889 | |||
| 9da6d92fb3 | |||
| 9dee5429d5 | |||
| ebe90aac5c | |||
| 289d69c79d | |||
| 5bdaf4f662 | |||
| 67f47bc3df | |||
| 67ddd76ff3 | |||
| 95de883879 | |||
| 3227da122e | |||
| ae243c783a | |||
| eb3b7f5146 | |||
| 414439cac9 | |||
| 7dacccb70e | |||
| a5e1d0595e | |||
| 7e8c6e92a2 | |||
| 24542d4e21 | |||
| b975125af5 | |||
| cc8c60605b | |||
| 23adc15ce3 | |||
| 7204aa0c9a | |||
| 6750429cd4 | |||
| cfe0a6cbb8 | |||
| a9f95971e8 | |||
| d7eee844b7 | |||
| 4c52731d16 | |||
| e53447d0bf | |||
| 53ed23ac58 | |||
| 226fb4a349 | |||
| d89632eb9d | |||
| 4621196993 | |||
| 988e753c74 | |||
| aa49b2ca76 | |||
| fdcd2fdb27 | |||
| 62f94206bb | |||
| 7b6647cfd4 | |||
| 73828c5db7 | |||
| e1d942bb3a | |||
| a764cf64b3 | |||
| 018d51993a | |||
| 4ce51291de | |||
| e0cd68787d | |||
| 6601f4c505 | |||
| 6f62f0e2f1 | |||
| c00026e203 | |||
| 38ac365953 | |||
| 7cfb0f7d2d | |||
| bae194f366 | |||
| 398d95528d | |||
| d1b2ee5490 | |||
| d39b9bc060 | |||
| 0c0b7d86ee | |||
| 03c4d9504f | |||
| 0737bfc5fe | |||
| 80b4916094 | |||
| 00845b43b4 | |||
| 83a74808d5 | |||
| 951679cc10 | |||
| 3cd95f06b2 | |||
| de00fa91ae | |||
| 25d4f0b347 | |||
| 413aa28594 | |||
| 29e153bc54 | |||
| 7b1a2f60d0 | |||
| 2302626703 | |||
| 28b3193743 | |||
| 0fdf8d0f6a | |||
| 890c93a820 | |||
| 3bfcde99dc | |||
| d5a8d19b66 | |||
| 7a3a9a6f7c | |||
| a816c52699 | |||
| bdd3157a45 | |||
| 7337e75d1e | |||
| b39f1694c3 | |||
| b3a919293c | |||
| 927eb1d0ba | |||
| b812c6d984 | |||
| 6a768c3f84 | |||
| 83f4e65bc3 | |||
| ad33abe6bc | |||
| 2e1624235c | |||
| 647f09f2f3 | |||
| d0ad2bd38b | |||
| 3ae4e11bbe | |||
| 6f19a3e750 | |||
| e8442eedad | |||
| a606db25f6 | |||
| 82f715bab6 | |||
| 28914b878c | |||
| 3c2f1709cc | |||
| 40ffde33b6 | |||
| 8184c21a80 | |||
| 5c13d0c2df | |||
| fd8fd22bdc | |||
| 31d8669c9d | |||
| 59e1fbf565 | |||
| 80c9bb0317 | |||
| 6c71bad8ef | |||
| 0e82f6287a | |||
| 52aacc1e1d | |||
| 909b4a5432 | |||
| b48b724c01 | |||
| 91ac1567d4 | |||
| c034035aba | |||
| cd852f9563 | |||
| 010ac6e5b6 | |||
| 0a3cf46068 | |||
| 2bbeeb860d | |||
| b74045fe55 | |||
| a940102d83 | |||
| 74e9d5eb3c | |||
| b256d66374 | |||
| 5b23e60b04 | |||
| 4cd71decbc | |||
| 1ba372e2c3 | |||
| d88f8c87f6 | |||
| ff02ab2aa0 | |||
| 38c71bdb3b | |||
| b68ce9695c | |||
| 0d3e7bd996 | |||
| 1e0acf4ce8 | |||
| b9e8e084a8 | |||
| 1701d4253d | |||
| ff60b71321 | |||
| f68439b936 | |||
| a3fe3944b7 | |||
| 9cf44777e1 | |||
| 5206c2c971 | |||
| 294fd10304 | |||
| 32650f8b9a | |||
| c5d96db9a8 | |||
| d04c66006b | |||
| d43529dce9 | |||
| 5ba95f3cbc | |||
| 7381e5b393 | |||
| b62f00dfd7 | |||
| 95787fcc66 | |||
| 4ef76f72dc | |||
| 939f33e791 | |||
| e15ade91d4 | |||
| 0776cc884d | |||
| 0ad09eb11f | |||
| 9e8b37c6cf | |||
| c2839934b4 | |||
| e2e3c3a9b3 | |||
| 9b3f4bc1a3 | |||
| cf9c34950f | |||
| d4a2927279 | |||
| 56172b1e5c | |||
| 7def5437fd | |||
| d21e364f11 | |||
| 7c6440fd9a | |||
| 0b61207ecf | |||
| fff2f40924 | |||
| 2166929fa8 | |||
| 5aa53da864 | |||
| 21d7ff2f74 | |||
| 8bba30e290 | |||
| fd94a32b95 | |||
| 25f9aa8c2d | |||
| 172bb4527c | |||
| 8ad769569c | |||
| 32eba22628 | |||
| 8d4179b5df | |||
| b7385825b6 | |||
| 5a20528cc8 | |||
| 324cf161ec | |||
| 9f84e77f57 | |||
| beaf0a9ecb | |||
| fb308e8038 | |||
| d2d15dae9f | |||
| 977f064157 | |||
| 8d23dd0186 | |||
| 90ae06df2a | |||
| 2cf253a914 | |||
| 9e462cc329 | |||
| 36ae8910b6 | |||
| fe0ed0bc9a | |||
| ec738515b7 | |||
| 287b6c6bc7 | |||
| aad93b9147 | |||
| 31176245bd | |||
| da45c7e2b9 | |||
| 3c73cb3fed | |||
| 275d01421e | |||
| 75edfccc31 | |||
| 1d230d9fed | |||
| 7d87dbf2ed | |||
| 44467331a2 | |||
| 29cbfd4fb3 | |||
| d946408105 | |||
| 6101372ba8 | |||
| 4993860a18 | |||
| 5e29119e83 | |||
| fe14664cfd | |||
| f871817a2f | |||
| 6ccacaa92e | |||
| 24e0f04aca | |||
| f878d499a7 | |||
| c24cd1c02b | |||
| c6028fca44 | |||
| 25daa5b358 | |||
| 0503a2a517 | |||
| 28fd3d9b18 | |||
| 1a9af1dee4 | |||
| ce27de1af6 | |||
| c9d5334a38 | |||
| bd20b7b8e0 | |||
| 52f40c8c26 | |||
| 622dd6aba4 | |||
| 969a6a1f2c | |||
| 08eab9622d | |||
| 107960b67c | |||
| b394f5e43f | |||
| 4d71a5c9d4 | |||
| 59e2216861 | |||
| 2fba5da04e | |||
| efbe2ddbf7 | |||
| 7f353723eb | |||
| a94d50f3cd | |||
| ae66e6f01a | |||
| e4b0b1e305 | |||
| 65ca2560fc | |||
| bdbf04efe9 | |||
| 4b56671d2d | |||
| f2b190b21a | |||
| 4f5cf931be | |||
| 68beefdaf2 | |||
| 4a52fd2edc | |||
| 47fdfb344d | |||
| ebbbc85d7b | |||
| 22ff2c36d9 | |||
| 5db2a1bb84 | |||
| 8a4ae0df20 | |||
| 80c3e48f5c | |||
| a7244032e4 | |||
| 7fc3ff26bc | |||
| 0648d230fc | |||
| 2626608200 | |||
| 081ef45047 | |||
| 9aa9ebc23f | |||
| f664a10024 | |||
| 52beb2f012 | |||
| 149e3c2d84 | |||
| 223965757c | |||
| 27c1d2516b | |||
| 014a97fcdc | |||
| eb85cc5c14 | |||
| b1d49d71ea | |||
| 9a0a6305e7 | |||
| a218bce73b | |||
| c94ca54aff | |||
| 2e8cd805fd | |||
| 1a9bb4ddc5 | |||
| e7f7c0704a | |||
| b5e0fb0e97 | |||
| f555989a5c | |||
| 2e0ea4520c | |||
| 31a9164df1 | |||
| b363a1e60d | |||
| e5714f9047 | |||
| e9b9c77edc | |||
| acd49994bd | |||
| 5264674cda | |||
| 2825925781 | |||
| 083bd7f1cf | |||
| 3f90fe04b4 | |||
| a63a247b22 | |||
| 7bb589b8f7 | |||
| 7c94ea3ec5 | |||
| 5789ae3c73 | |||
| 95fad3deb8 | |||
| 2b5b8d7931 | |||
| 96e0fb81ae | |||
| e5a568f510 | |||
| 8cb3155ea6 | |||
| 52c5d93449 | |||
| 5e3502f589 | |||
| f1fd36d3a1 | |||
| 9f49b7f4c0 | |||
| eca89e538b | |||
| bbac63e162 | |||
| ed3123470a | |||
| 170bda3158 | |||
| b5a8eec650 | |||
| 817b4ed133 | |||
| e4837cea9d | |||
| e1cdeb284f | |||
| f195933cb6 | |||
| 6e1d91b413 | |||
| d4e39de67e | |||
| 4903cfc689 | |||
| 54a8e26c7c | |||
| 4cc9006c8e | |||
| bc6492b1c4 | |||
| afd05f93fc | |||
| e83f5e7cca | |||
| 7b8e02023e | |||
| 3882f531cf | |||
| 0ce58b5c0f | |||
| f36a1c60f4 | |||
| 98fcc5d2d4 | |||
| 6ff2ee23f8 | |||
| e1405b140f | |||
| 04ab709ddd | |||
| 0736c68d77 | |||
| 34ccf345cd | |||
| 605a38d8ab | |||
| c9fd8ccae2 | |||
| 43482b1b96 | |||
| 81912b2530 | |||
| 2db2e7a369 | |||
| 3589d26fb3 | |||
| e5282fb264 | |||
| 61fddbb8af | |||
| e141aa3ff4 | |||
| 2fa1536315 | |||
| 5607dcfae9 | |||
| bd103802f9 | |||
| 0cbf427a58 | |||
| e53cd477f9 | |||
| df61f29a02 | |||
| feda01ab3b | |||
| fb2684c0b3 | |||
| 89e4b80e65 | |||
| d55fe2d100 | |||
| 49a510aaf9 | |||
| 16a5504a32 | |||
| b720c27c2a | |||
| cad3453e32 | |||
| afe0a8a159 | |||
| 2487e815a3 | |||
| 771348dd5e | |||
| 1f76e1228a | |||
| fe782e5ff8 | |||
| b4167a38f2 | |||
| 61459a197f | |||
| 30b6cc646a | |||
| df95d4e29f | |||
| 5090b64e57 | |||
| 12d188d85a | |||
| ab3e45d933 | |||
| 8c979cabd7 | |||
| a617922c1d | |||
| fec7cd65a7 | |||
| 10cfdb2fa2 | |||
| 43a62d8bec | |||
| 7b32b7ecd6 | |||
| 2fcd166d32 | |||
| d8c673157b | |||
| 99316df879 | |||
| 5a4fa3d2b3 | |||
| 71ececd7ce | |||
| 74519c08f0 | |||
| a85f19e016 | |||
| cfe887341d | |||
| eab17c3467 | |||
| 87c598627f | |||
| 30dc840144 | |||
| dd0e2d0bd2 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Adding support for trigger source in the run context, and make sure dynamic trigger runs are preprocessed so they have a chance of populating run properties
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/github": patch
|
||||
---
|
||||
|
||||
Fixed the broken export issue with two of the new tasks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fix for Next.js projects using the src dir
|
||||
@@ -8,11 +8,7 @@
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"wss",
|
||||
"webapp",
|
||||
"integrations",
|
||||
"emails",
|
||||
"internal-cli",
|
||||
"integration-catalog"
|
||||
"emails"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@trigger.dev/integration-kit": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/slack": patch
|
||||
"@trigger.dev/nextjs": patch
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Creating the init CLI package
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@trigger.dev/sdk": major
|
||||
"@trigger.dev/github": major
|
||||
"@trigger.dev/slack": major
|
||||
"@trigger.dev/nextjs": major
|
||||
---
|
||||
|
||||
Preparing packages for V2
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed the version of the @trigger.dev/nextjs package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Endpoint registration and indexing now is only initiated outside of clients
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Add support for task errors and task retrying
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Improved the CLI prompts
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Fixed the eventTrigger name
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/slack": patch
|
||||
---
|
||||
|
||||
Added support for backgroundFetch
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "next",
|
||||
"initialVersions": {
|
||||
"webapp": "1.0.0",
|
||||
"@trigger.dev/github": "0.2.0",
|
||||
"@trigger.dev/slack": "0.2.0",
|
||||
"create-trigger": "0.2.0",
|
||||
"emails": "1.0.0",
|
||||
"@trigger.dev/internal": "0.0.1",
|
||||
"@trigger.dev/nextjs": "0.1.0",
|
||||
"@trigger.dev/sdk": "1.0.0",
|
||||
"@trigger.dev/openai": "1.0.0-next.0",
|
||||
"@trigger.dev/database": "0.0.0",
|
||||
"@trigger.dev/init": "0.2.0",
|
||||
"@trigger.dev/integration-kit": "2.0.0-next.1",
|
||||
"@trigger.dev/resend": "1.0.0-next.1"
|
||||
},
|
||||
"changesets": [
|
||||
"bright-horses-bathe",
|
||||
"chilly-meals-warn",
|
||||
"clever-bats-learn",
|
||||
"curly-hornets-sort",
|
||||
"dirty-needles-kneel",
|
||||
"eighty-queens-prove",
|
||||
"four-ligers-perform",
|
||||
"great-meals-listen",
|
||||
"heavy-mails-explain",
|
||||
"itchy-rockets-provide",
|
||||
"modern-emus-suffer",
|
||||
"quick-stingrays-own",
|
||||
"strong-cars-visit",
|
||||
"stupid-files-cough",
|
||||
"sweet-snails-refuse",
|
||||
"tame-flowers-invite",
|
||||
"tough-walls-share",
|
||||
"wild-rats-mix"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed the help defaults
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed an issue that cause environment variables to be incorrectly added to .env.local
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Loosen the peer dep requirement on next
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/init": patch
|
||||
---
|
||||
|
||||
Fixed CLI showing undefined when not specifying a trigger-url flag
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/resend": patch
|
||||
---
|
||||
|
||||
Adding the resend integration package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Make sure nextjs app routes have the correct types
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Update the devDependency on nextjs to be more permissive
|
||||
@@ -1,8 +0,0 @@
|
||||
# You need to stop and re-run docker to switch, `pnpm run docker:db`
|
||||
# To login using GitHub and connect via OAuth with most integrations
|
||||
AUTH_CALLBACK_URL=http://localhost:3004/oauth/callback
|
||||
# To connect with Slack
|
||||
# AUTH_CALLBACK_URL=https://<ngrok subdomain>.eu.ngrok.io/oauth/callback
|
||||
|
||||
# You only need to use this locally if you specify PIZZLY_SECRET_KEY in the webapp .env file
|
||||
# PIZZLY_SECRET_KEY=<secret_key>
|
||||
@@ -0,0 +1,36 @@
|
||||
# YOU MUST SET THESE VARIABLES
|
||||
# Resend is an email service used for signing in to Trigger.dev via a Magic Link.
|
||||
### Visit https://resend.com, create an account and get your API key. Then insert it below along with your From and Reply To email addresses. Visit https://resend.com/docs for more information.
|
||||
RESEND_API_KEY=<api_key>
|
||||
FROM_EMAIL=
|
||||
REPLY_TO_EMAIL=
|
||||
|
||||
# YOU MIGHT LIKE TO MODIFY THESE VARIABLES
|
||||
SESSION_SECRET=abcdef1234
|
||||
MAGIC_LINK_SECRET=abcdef1234
|
||||
LOGIN_ORIGIN=http://localhost:3030
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public
|
||||
REMIX_APP_PORT=3030
|
||||
APP_ENV=development
|
||||
APP_ORIGIN=http://localhost:3030
|
||||
NODE_ENV=development
|
||||
|
||||
# OPTIONAL VARIABLES
|
||||
AUTH_GITHUB_CLIENT_ID=
|
||||
AUTH_GITHUB_CLIENT_SECRET=
|
||||
|
||||
# CLOUD VARIABLES
|
||||
POSTHOG_PROJECT_KEY=
|
||||
PLAIN_API_KEY=
|
||||
SEED_CLOUD=
|
||||
SEED_CLOUD_EMAIL=
|
||||
SEED_CLOUD_GITHUB_ACCESS_TOKEN=
|
||||
SEED_CLOUD_SLACK_ACCESS_TOKEN=
|
||||
SEED_CLOUD_USER_GITHUB_ACCESS_TOKEN=
|
||||
CLOUD_AIRTABLE_CLIENT_ID=
|
||||
CLOUD_AIRTABLE_CLIENT_SECRET=
|
||||
CLOUD_GITHUB_CLIENT_ID=
|
||||
CLOUD_GITHUB_CLIENT_SECRET=
|
||||
CLOUD_SLACK_APP_HOST=
|
||||
CLOUD_SLACK_CLIENT_ID=
|
||||
CLOUD_SLACK_CLIENT_SECRET=
|
||||
@@ -0,0 +1,153 @@
|
||||
name: 🚀 Publish Trigger.dev Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
env:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: ʦ TypeScript
|
||||
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
|
||||
|
||||
publish:
|
||||
needs: [typecheck]
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
short_sha: ${{ steps.get_commit.outputs.sha_short }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🆚 Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
elif [[ $GITHUB_REF == refs/heads/* ]]; then
|
||||
IMAGE_TAG="${GITHUB_REF#refs/heads/}"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
else
|
||||
echo "Invalid reference: ${GITHUB_REF}"
|
||||
exit 1
|
||||
fi
|
||||
echo ::set-output name=version::${IMAGE_TAG}
|
||||
|
||||
- name: 🔢 Get the commit hash
|
||||
id: get_commit
|
||||
run: |
|
||||
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
- name: 🐳 Build Docker Image
|
||||
run: |
|
||||
docker build -t release_build_image -f ./docker/Dockerfile .
|
||||
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐙 Push to GitHub Container Registry
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
- name: 🐙 Push 'latest' to GitHub Container Registry
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
|
||||
docker push $REGISTRY/$REPOSITORY:latest
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
|
||||
- name: 🌐 Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::067435167850:role/GitHubAction-AssumeRoleWithAction
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: 🌐 Login to ECR
|
||||
id: ecr-login
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
|
||||
- name: 🌐 Push to Amazon ECR
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
- name: 🌐 Push 'latest' to Amazon ECR
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
|
||||
docker push $REGISTRY/$REPOSITORY:latest
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
|
||||
- name: 🌐 Push commit hash to Amazon ECR
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}-${{ steps.get_commit.outputs.sha_short }}
|
||||
|
||||
- name: 🚀 Trigger Test Deploy
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && github.repository == 'triggerdotdev/trigger.dev'
|
||||
env:
|
||||
DEPLOY_TEST_HOOK: ${{ secrets.DEPLOY_TEST_HOOK }}
|
||||
run: |
|
||||
curl -X POST $DEPLOY_TEST_HOOK
|
||||
@@ -0,0 +1,67 @@
|
||||
name: 🦋 Changesets Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: 🦋 Changesets Release
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
if: |
|
||||
github.repository == 'triggerdotdev/trigger.dev'
|
||||
outputs:
|
||||
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
published: ${{ steps.changesets.outputs.published }}
|
||||
steps:
|
||||
- name: 🛑 Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
|
||||
- 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
|
||||
|
||||
- name: 🔐 Setup npm auth
|
||||
run: |
|
||||
echo "registry=https://registry.npmjs.org" >> ~/.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc
|
||||
|
||||
# This action has two responsibilities. The first time the workflow runs
|
||||
# (initial push to the `main` branch) it will create a new branch and
|
||||
# then open a PR with the related changes for the new version. After the
|
||||
# PR is merged, the workflow will run again and this action will build +
|
||||
# publish to npm.
|
||||
- name: 🚀 PR / Publish
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm run changeset:version
|
||||
commit: "chore: Update version for release"
|
||||
title: "chore: Update version for release"
|
||||
publish: pnpm run changeset:release
|
||||
createGithubReleases: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
+3
-1
@@ -46,4 +46,6 @@ yarn-error.log*
|
||||
apps/**/public/build
|
||||
.tests-container-id.txt
|
||||
.sentryclirc
|
||||
.buildt
|
||||
.buildt
|
||||
|
||||
**/tmp/
|
||||
@@ -1 +1,2 @@
|
||||
link-workspace-packages=false
|
||||
link-workspace-packages=false
|
||||
public-hoist-pattern[]=*prisma*
|
||||
@@ -1,73 +0,0 @@
|
||||
# Warp Launch Configuration
|
||||
#
|
||||
#
|
||||
# Use this to start a certain configuration of windows, tabs, and panes
|
||||
# Open the launch configuration palette to access and open any launch configuration
|
||||
#
|
||||
# This file defines your launch configuration
|
||||
# More on how to do so here:
|
||||
# https://docs.warp.dev/features/session-management/launch-configurations
|
||||
#
|
||||
# All launch configurations are stored under ~/.warp/launch_configurations/
|
||||
# Edit them anytime!
|
||||
#
|
||||
# You can also add commands that run on-start for your launch configurations like so:
|
||||
# ---
|
||||
# name: Example with Command
|
||||
# windows:
|
||||
# - tabs:
|
||||
# - layout:
|
||||
# cwd: /Users/warp-user/project
|
||||
# commands:
|
||||
# - exec: code .
|
||||
|
||||
---
|
||||
name: Triggerdotdev
|
||||
windows:
|
||||
- tabs:
|
||||
- title: webapp
|
||||
layout:
|
||||
split_direction: vertical
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: pnpm run build --filter webapp
|
||||
- exec: pnpm run i-dev --filter webapp
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/webapp
|
||||
- title: integrations
|
||||
layout:
|
||||
split_direction: vertical
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: pnpm run dev --filter integrations
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/integrations
|
||||
- title: ngrok
|
||||
layout:
|
||||
split_direction: horizontal
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-webapp.sh <your-trigger-dev>
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-pizzly.sh <your-pizzly-dev>
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-wss.sh <your-wss-dev>
|
||||
- title: wss and examples
|
||||
layout:
|
||||
split_direction: horizontal
|
||||
panes:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/wss
|
||||
commands:
|
||||
- exec: LOG_LEVEL=debug pnpm run dev
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/examples
|
||||
- title: emails
|
||||
layout:
|
||||
split_direction: horizontal
|
||||
panes:
|
||||
- cwd: /Users/eric/Work/Git/APIHero/trigger.dev/packages/emails
|
||||
commands:
|
||||
- exec: pnpm run dev
|
||||
- cwd: /Users/eric/Work/Git/APIHero/trigger.dev/packages/emails
|
||||
+9
-10
@@ -4,23 +4,22 @@ Trigger.dev uses [changesets](https://github.com/changesets/changesets) to manag
|
||||
|
||||
## Adding a changeset
|
||||
|
||||
To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset, which currently are:
|
||||
To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset.
|
||||
|
||||
- `@trigger.dev/sdk`
|
||||
- `@trigger.dev/integration-sdk`
|
||||
- `@trigger.dev/github`
|
||||
- `@trigger.dev/notion`
|
||||
- `@trigger.dev/slack`
|
||||
- `@trigger.dev/shopify`
|
||||
- `@trigger.dev/resend`
|
||||
|
||||
## Release instructions
|
||||
## Release instructions (local only)
|
||||
|
||||
Based on the instructions [here](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md)
|
||||
|
||||
1. Run `pnpm run changeset:version`
|
||||
2. Run `pnpm run changeset:release`
|
||||
|
||||
## Release instructions (CI)
|
||||
|
||||
Please follow the best-practice of adding changesets in the same commit as the code making the change with `pnpm run changeset:add`, as it will allow our release.yml CI workflow to function properly:
|
||||
|
||||
- Anytime new changesets are added in a commit in the `main` branch, the [release.yml](./.github/workflows/release.yml) workflow will run and will automatically create/update a PR with a fresh run of `pnpm run changeset:version`.
|
||||
- When the version PR is merged into `main`, the release.yml workflow will automatically run `pnpm run changeset:release` to build and release packages to npm.
|
||||
|
||||
## Pre-release instructions
|
||||
|
||||
1. Add changesets as usual `pnpm run changeset:add`
|
||||
|
||||
@@ -1,25 +1,201 @@
|
||||
Copyright (c) 2022 API Hero Inc.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
- All content that resides under any "ee/" directory of this repository, if such directories exists, are licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Trigger.dev Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
1. Definitions.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -4,18 +4,102 @@
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/3f5ad4c1-c4c8-4277-b622-290e7f37bd00/public">
|
||||
<img alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/a45d1fa2-0ae8-4a39-4409-f4f934bfae00/public">
|
||||
</picture>
|
||||
|
||||
### The background jobs framework for Next.js
|
||||
|
||||
[Discord](https://discord.gg/bpVDSjcG) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
|
||||
|
||||
[](https://twitter.com/triggerdotdev)
|
||||
[](https://github.com/triggerdotdev/trigger.dev)
|
||||
|
||||
</div>
|
||||
|
||||
# **The background jobs framework for Next.js**
|
||||
|
||||
# About Trigger.dev
|
||||
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
|
||||
|
||||
|
||||
### Long running Jobs on serverless
|
||||
|
||||
### _**Trigger.dev v2 is launching soon...**_
|
||||
Reliably run jobs and don’t worry about function timeouts, we handle those for you.
|
||||
|
||||
Visit the new landing page for [Trigger.dev v2](https://next.trigger.dev) and [sign up for early access.](https://bcymafitv0e.typeform.com/to/QQnotGJM#source=github)
|
||||
- Auto-resume after a function timeout
|
||||
- Auto-resume after a server outage
|
||||
- Add delays of up to a year
|
||||
|
||||
### In your codebase
|
||||
|
||||
Create Jobs where they belong: in your codebase. Version control, localhost, test, review, and deploy like you're already used to.
|
||||
|
||||
### Secure by design
|
||||
|
||||
We only receive Triggers and the data you choose to send to us. You can even completely self-host the entire platform.
|
||||
|
||||
### Don't worry about deployment
|
||||
|
||||
Just use our SDK to write Jobs in your Next.js codebase. There's nothing extra to deploy and no CI to configure, your Jobs just connect to our cloud. Or you can always self-host.
|
||||
|
||||
### Full visibility of every job run
|
||||
|
||||
View every Task in every Run so you can tell exactly what happened.
|
||||
|
||||

|
||||
|
||||
### Built-in integrations
|
||||
|
||||
Easily integrate with hundreds of third-party APIs – including your own. Use API keys (which never leave your server) or let us handle OAuth for you. Install our integration packages and easily subscribe to webhooks and perform common tasks, or you can easily use your existing favorite Node.JS SDKs and get resumability and idempotency through our `runTask` function.
|
||||
|
||||
### Trigger.dev Connect (coming soon)
|
||||
|
||||
Easily add integrations for your users.
|
||||
|
||||
# Getting started
|
||||
|
||||
Visit our docs [here](https://trigger.dev/docs).
|
||||
|
||||
## Self-host
|
||||
|
||||
We provide an official trigger.dev docker image you can use to easily self-host the platform. We're working on more extensive guides but we currently provide a [Fly.io example repository](https://github.com/triggerdotdev/fly.io) with instructions in the README for deploying and using a self-hosted instance of Trigger.dev on Fly.io.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
- [Node.js](https://nodejs.org/en) version >=18.x
|
||||
- [pnpm package manager](https://pnpm.io/installation) version 7
|
||||
- [Docker](https://www.docker.com/get-started/)
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repo into a public GitHub repository or [fork the repo](https://github.com/triggerdotdev/trigger.dev/fork). If you plan to distribute the code, keep the source code public to comply with the [Apache Licence 2.0](https://github.com/triggerdotdev/trigger.dev/blob/main/LICENSE).
|
||||
|
||||
```
|
||||
git clone https://github.com/triggerdotdev/trigger.dev.git
|
||||
```
|
||||
> If you are on windows, run the following command on gitbash with admin privileges:
|
||||
> `git clone -c core.symlinks=true https://triggerdotdev/trigger.dev.git`
|
||||
2. Navigate to the project folder
|
||||
```
|
||||
cd trigger.dev
|
||||
```
|
||||
3. Install the required packages using pnpm.
|
||||
```
|
||||
pnpm i
|
||||
```
|
||||
4. Create your `.env` files
|
||||
```
|
||||
cp .env.example .env && cp packages/database/.env.example packages/database/.env
|
||||
```
|
||||
Open the root `.env` file and fill in the required values.
|
||||
6. Start Docker. This starts the required services like Postgres.
|
||||
```
|
||||
pnpm run docker
|
||||
```
|
||||
7. Migrate the database
|
||||
```
|
||||
pnpm run db:migrate
|
||||
```
|
||||
8. Run the seed script
|
||||
```
|
||||
pnpm run db:seed
|
||||
```
|
||||
5. Run the project. It should run on `port:3030`
|
||||
```
|
||||
pnpm run dev --filter webapp
|
||||
```
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
|
||||
TRIGGER_API_KEY=
|
||||
|
||||
TRIGGER_LOG_LEVEL=debug
|
||||
|
||||
|
||||
|
||||
##########################################################################################
|
||||
# ************************************* BASICS *******************************************
|
||||
##########################################################################################
|
||||
|
||||
APP_ENV=development
|
||||
|
||||
APP_ORIGIN=http://localhost:3000
|
||||
|
||||
NODE_ENV=development
|
||||
|
||||
PULSAR_ENABLED=1
|
||||
|
||||
SESSION_SECRET=
|
||||
|
||||
|
||||
|
||||
#########################################################################################
|
||||
# ************************************* LOGIN *******************************************
|
||||
#########################################################################################
|
||||
|
||||
GITHUB_CLIENT_ID=
|
||||
|
||||
GITHUB_SECRET=
|
||||
|
||||
LOGIN_ORIGIN=http://localhost:3000
|
||||
|
||||
MAGIC_LINK_SECRET=
|
||||
|
||||
|
||||
|
||||
############################################################################################
|
||||
# ************************************ OPTIONAL ********************************************
|
||||
############################################################################################
|
||||
|
||||
POSTHOG_PROJECT_KEY=
|
||||
|
||||
|
||||
|
||||
############################################################################################
|
||||
# ************************************ DATABASE ********************************************
|
||||
############################################################################################
|
||||
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
|
||||
|
||||
|
||||
|
||||
################################################################################################
|
||||
# ********************************** INTEGRATIONS **********************************************
|
||||
################################################################################################
|
||||
|
||||
PIZZLY_HOST=http://localhost:3004
|
||||
|
||||
PIZZLY_SECRET_KEY=
|
||||
|
||||
|
||||
|
||||
#########################################################################################
|
||||
# ************************************* EMAIL *******************************************
|
||||
#########################################################################################
|
||||
|
||||
FROM_EMAIL=
|
||||
|
||||
REPLY_TO_EMAIL=
|
||||
|
||||
RESEND_API_KEY=<api_key>
|
||||
@@ -3,6 +3,7 @@ import "../app/tailwind.css";
|
||||
import { unstable_createRemixStub } from "@remix-run/testing";
|
||||
import React from "react";
|
||||
import { LocaleContextProvider } from "../app/components/primitives/LocaleProvider";
|
||||
import { OperatingSystemContextProvider } from "../app/components/primitives/OperatingSystemProvider";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
@@ -33,9 +34,13 @@ const preview: Preview = {
|
||||
]);
|
||||
|
||||
return (
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixStub initialEntries={["/"]} />
|
||||
</LocaleContextProvider>
|
||||
<OperatingSystemContextProvider platform="mac">
|
||||
<LocaleContextProvider
|
||||
locales={window.navigator.languages as string[]}
|
||||
>
|
||||
<RemixStub initialEntries={["/"]} />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
FROM node:16-bullseye AS pruner
|
||||
RUN apt-get update && apt-get install openssl -y
|
||||
WORKDIR /app
|
||||
RUN npm install turbo@1.7.0 -g
|
||||
COPY . .
|
||||
RUN turbo prune --scope=webapp --docker
|
||||
RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
||||
|
||||
# Base strategy to have layer caching
|
||||
FROM node:16-bullseye AS base
|
||||
RUN apt-get update && apt-get install openssl ca-certificates g++ make wget python3 -y
|
||||
ENV PULSAR_CPP_CLIENT_VERSION=2.10.3
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client.deb -q
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client-dev.deb -q
|
||||
RUN dpkg -i ./apache-pulsar-client*.deb
|
||||
WORKDIR /app
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||
|
||||
FROM base AS production-deps
|
||||
WORKDIR /app
|
||||
RUN npm install turbo@1.7.0 -g
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV production
|
||||
RUN npm config set python /usr/bin/python3
|
||||
COPY --from=pruner /app/out/full/apps/webapp/prisma/schema.prisma /app/apps/webapp/prisma/schema.prisma
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
RUN pnpx prisma generate --schema /app/apps/webapp/prisma/schema.prisma
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
RUN npm install turbo@1.7.0 -g
|
||||
COPY turbo.json turbo.json
|
||||
RUN corepack enable
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
RUN npm config set python /usr/bin/python3
|
||||
ENV NODE_ENV development
|
||||
RUN pnpm install --ignore-scripts --frozen-lockfile
|
||||
ENV NODE_ENV production
|
||||
RUN pnpm run generate
|
||||
RUN pnpm run build --filter=webapp...
|
||||
RUN pnpx prisma migrate deploy --schema apps/webapp/prisma/schema.prisma
|
||||
RUN pnpm run db:seed --filter=webapp
|
||||
|
||||
# Runner
|
||||
FROM node:16-bullseye AS runner
|
||||
RUN apt-get update && apt-get install openssl ca-certificates g++ make wget python3 -y
|
||||
ENV PULSAR_CPP_CLIENT_VERSION=2.10.3
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client.deb -q
|
||||
RUN wget https://archive.apache.org/dist/pulsar/pulsar-${PULSAR_CPP_CLIENT_VERSION}/DEB/apache-pulsar-client-dev.deb -q
|
||||
RUN dpkg -i ./apache-pulsar-client*.deb
|
||||
RUN npm install turbo -g
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV production
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 remixjs
|
||||
RUN chown -R remixjs:nodejs /app
|
||||
USER remixjs
|
||||
|
||||
COPY --from=pruner --chown=remixjs:nodejs /app/out/full/ .
|
||||
COPY --from=production-deps --chown=remixjs:nodejs /app .
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/app/styles/tailwind.css ./apps/webapp/app/styles/tailwind.css
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/build/server.js ./apps/webapp/build/server.js
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/build ./apps/webapp/build
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/public ./apps/webapp/public
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/prisma/schema.prisma ./apps/webapp/build/schema.prisma
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/prisma/migrations ./apps/webapp/build/migrations
|
||||
COPY --from=builder --chown=remixjs:nodejs /app/apps/webapp/node_modules/.prisma/client/libquery_engine-debian-openssl-1.1.x.so.node ./apps/webapp/build/libquery_engine-debian-openssl-1.1.x.so.node
|
||||
|
||||
# release_command = "pnpx prisma migrate deploy --schema apps/webapp/prisma/schema.prisma"
|
||||
ENTRYPOINT ["pnpm", "--filter", "webapp", "run", "start"]
|
||||
@@ -0,0 +1,17 @@
|
||||
export function DynamicTriggerIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.0151 4C2.01629 2.89543 2.91269 2.00097 4.01726 2.00216L16.0172 2.0151C17.1218 2.01629 18.0163 2.91269 18.0151 4.01726L18.0021 16.0172C18.001 17.1218 17.1046 18.0163 16 18.0151L4 18.0021C2.89543 18.001 2.00097 17.1046 2.00216 16L2.0151 4ZM9.26429 4.75782C9.26473 4.34361 9.60088 4.00818 10.0151 4.00863C10.4293 4.00907 10.7647 4.34522 10.7643 4.75944L10.7606 8.1988L13.1952 5.76942C13.4884 5.47684 13.9633 5.47735 14.2559 5.77056C14.5484 6.06377 14.5479 6.53864 14.2547 6.83122L11.8201 9.26058L15.2594 9.26429C15.6736 9.26473 16.0091 9.60088 16.0086 10.0151C16.0082 10.4293 15.672 10.7647 15.2578 10.7643L11.8185 10.7606L14.2478 13.1952C14.5404 13.4884 14.5399 13.9633 14.2467 14.2558C13.9535 14.5484 13.4786 14.5479 13.186 14.2547L10.7567 11.8201L10.753 15.2594C10.7525 15.6736 10.4164 16.0091 10.0022 16.0086C9.58794 16.0082 9.25252 15.672 9.25296 15.2578L9.25667 11.8185L6.82209 14.2478C6.52888 14.5404 6.05401 14.5399 5.76143 14.2467C5.46885 13.9535 5.46937 13.4786 5.76258 13.186L8.19718 10.7567L4.75782 10.753C4.34361 10.7525 4.00818 10.4164 4.00863 10.0022C4.00907 9.58794 4.34522 9.25252 4.75944 9.25296L8.19877 9.25667L5.76942 6.82207C5.47684 6.52886 5.47735 6.05399 5.77056 5.76141C6.06377 5.46883 6.53864 5.46935 6.83122 5.76255L9.26058 8.19716L9.26429 4.75782Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ReactNode } from "react";
|
||||
import { MainCenteredContainer } from "./layout/AppLayout";
|
||||
import { Header1, Header2 } from "./primitives/Headers";
|
||||
import { NamedIcon, NamedIconInBox } from "./primitives/NamedIcon";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
|
||||
type ComingSoonProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
export function ComingSoon({ title, description, icon }: ComingSoonProps) {
|
||||
return (
|
||||
<MainCenteredContainer>
|
||||
<div className="flex flex-col justify-center rounded border border-slate-800 bg-slate-850">
|
||||
<div className="flex items-center gap-2 border-b border-slate-750 px-4 py-4">
|
||||
{typeof icon === "string" ? (
|
||||
<NamedIconInBox
|
||||
name={icon}
|
||||
className={"h-10 w-10 bg-midnight-800"}
|
||||
/>
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<div className="mt-0.5 flex flex-col">
|
||||
<Paragraph variant="extra-extra-small/caps">Coming soon</Paragraph>
|
||||
<Header1 className="-mt-0.5">{title}</Header1>
|
||||
</div>
|
||||
</div>
|
||||
<Paragraph variant="small" className="p-4">
|
||||
{description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</MainCenteredContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useLocation,
|
||||
useNavigation,
|
||||
} from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { feedbackTypeLabel, schema } from "~/routes/resources.feedback";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { Fieldset } from "./primitives/Fieldset";
|
||||
import { FormButtons } from "./primitives/FormButtons";
|
||||
import { FormError } from "./primitives/FormError";
|
||||
import { InputGroup } from "./primitives/InputGroup";
|
||||
import { Label } from "./primitives/Label";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./primitives/Select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTrigger,
|
||||
} from "./primitives/Sheet";
|
||||
import { TextArea } from "./primitives/TextArea";
|
||||
|
||||
export function Feedback() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const lastSubmission = useActionData();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const [form, { path, feedbackType, message }] = useForm({
|
||||
id: "accept-invite",
|
||||
lastSubmission,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
open &&
|
||||
navigation.formAction === "/resources/feedback" &&
|
||||
form.error === undefined &&
|
||||
form.errors.length === 0
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild={true}>
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
LeadingIcon={ChatBubbleLeftRightIcon}
|
||||
shortcut={{ key: "f" }}
|
||||
>
|
||||
Send us feedback
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent size="sm">
|
||||
<SheetHeader className="justify-between">Give us feedback</SheetHeader>
|
||||
<SheetBody>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
We'd love to hear your feedback, good, bad or ugly.
|
||||
</Paragraph>
|
||||
<Form method="post" action="/resources/feedback" {...form.props}>
|
||||
<Fieldset>
|
||||
<input
|
||||
value={location.pathname}
|
||||
{...conform.input(path, { type: "hidden" })}
|
||||
/>
|
||||
<InputGroup>
|
||||
<Label>What kind of feedback do you have?</Label>
|
||||
<SelectGroup>
|
||||
<Select {...conform.input(feedbackType)} defaultValue={"bug"}>
|
||||
<SelectTrigger size="medium" width="full">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(feedbackTypeLabel).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<FormError id={feedbackType.errorId}>
|
||||
{feedbackType.error}
|
||||
</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label>Message</Label>
|
||||
<TextArea {...conform.textarea(message)} />
|
||||
<FormError id={message.errorId}>{message.error}</FormError>
|
||||
</InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
<FormButtons
|
||||
className="max-w-md"
|
||||
confirmButton={
|
||||
<Button type="submit" variant="primary/medium">
|
||||
Send
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { darkTheme } from "./codeMirrorTheme";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
content: string;
|
||||
defaultValue?: string;
|
||||
language?: "json";
|
||||
readOnly?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
@@ -33,13 +33,14 @@ const defaultProps: JSONEditorDefaultProps = {
|
||||
|
||||
export function JSONEditor(opts: JSONEditorProps) {
|
||||
const {
|
||||
content,
|
||||
defaultValue = "",
|
||||
language,
|
||||
readOnly,
|
||||
onChange,
|
||||
onUpdate,
|
||||
onBlur,
|
||||
basicSetup,
|
||||
autoFocus,
|
||||
} = {
|
||||
...defaultProps,
|
||||
...opts,
|
||||
@@ -59,15 +60,15 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
extensions,
|
||||
editable: !readOnly,
|
||||
contentEditable: !readOnly,
|
||||
value: content,
|
||||
autoFocus: false,
|
||||
value: defaultValue,
|
||||
autoFocus,
|
||||
theme: darkTheme(),
|
||||
indentWithTab: false,
|
||||
basicSetup,
|
||||
onChange,
|
||||
onUpdate,
|
||||
};
|
||||
const { setContainer } = useCodeMirror(settings);
|
||||
const { setContainer, state } = useCodeMirror(settings);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
@@ -75,9 +76,21 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
}
|
||||
}, [setContainer]);
|
||||
|
||||
//if the defaultValue changes update the editor
|
||||
useEffect(() => {
|
||||
if (state !== undefined) {
|
||||
state.update({
|
||||
changes: { from: 0, to: state.doc.length, insert: defaultValue },
|
||||
});
|
||||
}
|
||||
}, [defaultValue, state]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("no-scrollbar overflow-y-auto", opts.className)}
|
||||
className={cn(
|
||||
"overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
|
||||
opts.className
|
||||
)}
|
||||
ref={editor}
|
||||
onBlur={() => {
|
||||
if (!onBlur) return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type Environment = Pick<RuntimeEnvironment, "type" | "slug">;
|
||||
type Environment = Pick<RuntimeEnvironment, "type">;
|
||||
|
||||
export function EnvironmentLabel({
|
||||
environment,
|
||||
@@ -13,7 +13,7 @@ export function EnvironmentLabel({
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm px-1 py-0.5 text-xxs font-medium uppercase text-midnight-900",
|
||||
"flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
|
||||
environmentColorClassName(environment),
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import integrationButton from "./integration-button.png";
|
||||
import ngrok from "./ngrok.png";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
|
||||
export function HowToSetupYourProject() {
|
||||
const devEnvironment = useDevEnvironment();
|
||||
console.log(devEnvironment);
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Run your Next.js app" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>Ensure your Next.js app is running locally.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run ngrok" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
There are a few ways to do this, but we recommend using{" "}
|
||||
<TextLink
|
||||
href="https://trigger.dev/docs/documentation/guides/tunneling-localhost"
|
||||
target="_blank"
|
||||
>
|
||||
ngrok
|
||||
</TextLink>
|
||||
. It’s free and easy to use and required to create a tunnel, making
|
||||
your local machine accessible to the internet.
|
||||
</Paragraph>
|
||||
<StepNumber stepNumber="a" title="Install ngrok" />
|
||||
<StepContentContainer>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mt-2"
|
||||
value={`brew install ngrok/ngrok/ngrok`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="b" title="Open a new terminal window/tab" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>You need to leave this running.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="c" title="Create an http tunnel at port 3000" />
|
||||
<StepContentContainer>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`ngrok http 3000`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="d"
|
||||
title="Grab your forwarding address in the ngrok output"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<img src={ngrok} className="mt-2 w-full" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="d" title="Use the forwarding URL" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Use the forwarding URL that ngrok gave you for the{" "}
|
||||
<InlineCode>VERCEL_URL</InlineCode> environment variable.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Use the CLI" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Run this CLI command in a new terminal window.
|
||||
</Paragraph>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npx @trigger.dev/init -k ${devEnvironment?.apiKey} -t https://test-cloud.trigger.dev`}
|
||||
/>
|
||||
<Paragraph spacing>
|
||||
The CLI will add Trigger.dev to your existing Next.js project, setup a
|
||||
route and give you an example file.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Once you've run the CLI command, click Refresh to view your example
|
||||
Job in the list.
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="primary/medium"
|
||||
className="mt-4"
|
||||
LeadingIcon="refresh"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToRunATest() {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Step 1 title" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>Content</Paragraph>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToConnectAnIntegration() {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Select an API from the list" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
APIs marked with a
|
||||
<span
|
||||
className="mx-2 -mt-1 inline-flex"
|
||||
aria-label="Trigger.dev Integration icon"
|
||||
>
|
||||
<IntegrationIcon />
|
||||
</span>
|
||||
are Trigger.dev Integrations. These Integrations make connecting to
|
||||
the API easier by offering OAuth or API key authentication. All APIs
|
||||
can also be used with fetch or an SDK.
|
||||
</Paragraph>
|
||||
<img src={integrationButton} className="mt-2 h-10" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Choose how you want to connect" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Follow the instructions for your chosen connection method in the
|
||||
popover form. If no Integration exists yet, you can request one by
|
||||
clicking the "I want an Integration" button.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="3"
|
||||
title="Your connection will appear in the list"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Once you've connected your API, it will appear in the list of
|
||||
Integrations below. You can view details and manage your connection by
|
||||
selecting it from the table.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Callout
|
||||
variant={"docs"}
|
||||
href="https://trigger.dev/docs/integrations/introduction"
|
||||
>
|
||||
View the Integration docs page for more information on connecting an API
|
||||
using an Integration or other method.
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToUseThisIntegration() {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Step 1 title" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>Content</Paragraph>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StepContentContainer({ children }: { children: React.ReactNode }) {
|
||||
return <div className="mb-6 ml-9 mt-1">{children}</div>;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ApiAuthenticationMethodApiKey,
|
||||
Integration,
|
||||
} from "~/services/externalApis/types";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
|
||||
export function ApiKeyHelp({
|
||||
integration,
|
||||
apiAuth,
|
||||
}: {
|
||||
integration: Integration;
|
||||
apiAuth: ApiAuthenticationMethodApiKey;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Header1 className="mb-2">
|
||||
How to use {integration.name} with API keys
|
||||
</Header1>
|
||||
<Paragraph spacing>
|
||||
You can use API keys to authenticate with {integration.name}. Your API
|
||||
keys won't leave your server, we'll never see them.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
First install the <InlineCode>{integration.packageName}</InlineCode>{" "}
|
||||
package using your preferred package manager. For example:
|
||||
</Paragraph>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`npm install ${integration.packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
{apiAuth.help.samples.map((sample, i) => (
|
||||
<div key={i}>
|
||||
<Paragraph spacing>{sample.title}</Paragraph>
|
||||
<CodeBlock
|
||||
code={sample.code}
|
||||
className="mb-4"
|
||||
highlightedRanges={sample.highlight}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ApiAuthenticationMethodApiKey,
|
||||
Integration,
|
||||
} from "~/services/externalApis/types";
|
||||
import { docsIntegrationPath } from "~/utils/pathBuilder";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTrigger,
|
||||
} from "../primitives/Sheet";
|
||||
import { ApiKeyHelp } from "./ApiKeyHelp";
|
||||
import { CustomHelp } from "./CustomHelp";
|
||||
import { SelectOAuthMethod } from "./SelectOAuthMethod";
|
||||
|
||||
type IntegrationMethod = "apikey" | "oauth2" | "custom";
|
||||
|
||||
export function ConnectToIntegrationSheet({
|
||||
integration,
|
||||
organizationId,
|
||||
button,
|
||||
className,
|
||||
callbackUrl,
|
||||
}: {
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
button: React.ReactNode;
|
||||
callbackUrl: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [integrationMethod, setIntegrationMethod] = useState<
|
||||
IntegrationMethod | undefined
|
||||
>(undefined);
|
||||
|
||||
const authMethods = Object.values(integration.authenticationMethods);
|
||||
const hasApiKeyOption = authMethods.some((s) => s.type === "apikey");
|
||||
const hasOAuth2Option = authMethods.some((s) => s.type === "oauth2");
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger className={className}>{button}</SheetTrigger>
|
||||
<SheetContent size="lg">
|
||||
<SheetHeader>
|
||||
<NamedIconInBox name={integration.identifier} className="h-9 w-9" />
|
||||
<div className="grow">
|
||||
<Header2>{integration.name}</Header2>
|
||||
{integration.description && (
|
||||
<Paragraph variant="small">{integration.description}</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
<LinkButton
|
||||
to={docsIntegrationPath(integration.identifier)}
|
||||
variant="secondary/small"
|
||||
LeadingIcon="docs"
|
||||
>
|
||||
View docs
|
||||
</LinkButton>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<Header2 className="mb-2">Choose an integration method</Header2>
|
||||
<RadioGroup
|
||||
name="method"
|
||||
className="flex gap-2"
|
||||
value={integrationMethod}
|
||||
onValueChange={(v) => setIntegrationMethod(v as IntegrationMethod)}
|
||||
>
|
||||
{hasOAuth2Option && (
|
||||
<RadioGroupItem
|
||||
id="oauth2"
|
||||
value="oauth2"
|
||||
label="OAuth"
|
||||
description="We handle OAuth for you or your users."
|
||||
variant="description"
|
||||
/>
|
||||
)}
|
||||
{hasApiKeyOption && (
|
||||
<RadioGroupItem
|
||||
id="apikey"
|
||||
value="apikey"
|
||||
label="API Key"
|
||||
description="Use API keys in your code. They never leave your server."
|
||||
variant="description"
|
||||
/>
|
||||
)}
|
||||
<RadioGroupItem
|
||||
id="custom"
|
||||
value="custom"
|
||||
label="Fetch/Existing SDK"
|
||||
description={`Alternatively, use ${integration.name} without our integration.`}
|
||||
variant="description"
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
{integrationMethod && (
|
||||
<SelectedIntegrationMethod
|
||||
integration={integration}
|
||||
organizationId={organizationId}
|
||||
method={integrationMethod}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
)}
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectedIntegrationMethod({
|
||||
integration,
|
||||
organizationId,
|
||||
method,
|
||||
callbackUrl,
|
||||
}: {
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
method: IntegrationMethod;
|
||||
callbackUrl: string;
|
||||
}) {
|
||||
const authMethods = Object.values(integration.authenticationMethods);
|
||||
|
||||
switch (method) {
|
||||
case "apikey":
|
||||
const apiAuth = authMethods.find((a) => a.type === "apikey");
|
||||
if (!apiAuth) return null;
|
||||
return (
|
||||
<ApiKeyHelp
|
||||
integration={integration}
|
||||
apiAuth={apiAuth as ApiAuthenticationMethodApiKey}
|
||||
/>
|
||||
);
|
||||
case "oauth2":
|
||||
return (
|
||||
<SelectOAuthMethod
|
||||
integration={integration}
|
||||
organizationId={organizationId}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
);
|
||||
case "custom":
|
||||
return <CustomHelp name={integration.name} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
|
||||
import cuid from "cuid";
|
||||
import { useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { useTextFilter } from "~/hooks/useTextFilter";
|
||||
import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2";
|
||||
import {
|
||||
ApiAuthenticationMethodOAuth2,
|
||||
Integration,
|
||||
Scope,
|
||||
} from "~/services/externalApis/types";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Checkbox } from "../primitives/Checkbox";
|
||||
import { Fieldset } from "../primitives/Fieldset";
|
||||
import { FormError } from "../primitives/FormError";
|
||||
import { Header2, Header3 } from "../primitives/Headers";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { InputGroup } from "../primitives/InputGroup";
|
||||
import { Label } from "../primitives/Label";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import type { ConnectionType } from "@trigger.dev/database";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
|
||||
export type Status = "loading" | "idle";
|
||||
|
||||
export function ConnectToOAuthForm({
|
||||
integration,
|
||||
authMethod,
|
||||
authMethodKey,
|
||||
organizationId,
|
||||
clientType,
|
||||
callbackUrl,
|
||||
}: {
|
||||
integration: Integration;
|
||||
authMethod: ApiAuthenticationMethodOAuth2;
|
||||
authMethodKey: string;
|
||||
organizationId: string;
|
||||
clientType: ConnectionType;
|
||||
callbackUrl: string;
|
||||
}) {
|
||||
const [id] = useState<string>(cuid());
|
||||
const transition = useNavigation();
|
||||
const fetcher = useFetcher();
|
||||
const { isManagedCloud } = useFeatures();
|
||||
|
||||
const [
|
||||
form,
|
||||
{ title, scopes, hasCustomClient, customClientId, customClientSecret },
|
||||
] = useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
// Create the schema without any constraint defined
|
||||
schema: createSchema(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(
|
||||
new Set(
|
||||
authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)
|
||||
)
|
||||
);
|
||||
|
||||
const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud;
|
||||
|
||||
const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp);
|
||||
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<Scope>({
|
||||
items: authMethod.scopes,
|
||||
filter: (scope, text) => {
|
||||
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (
|
||||
scope.description &&
|
||||
scope.description.toLowerCase().includes(text.toLowerCase())
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={`/resources/connection/${organizationId}/oauth2`}
|
||||
{...form.props}
|
||||
className="flex h-full max-h-full flex-grow flex-col"
|
||||
>
|
||||
<Fieldset>
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationIdentifier"
|
||||
value={integration.identifier}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationAuthMethod"
|
||||
value={authMethodKey}
|
||||
/>
|
||||
<input type="hidden" name="redirectTo" value={location.pathname} />
|
||||
<InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
{...conform.input(title)}
|
||||
placeholder={`e.g. Personal ${integration.name}`}
|
||||
/>
|
||||
<FormError>{title.error}</FormError>
|
||||
</InputGroup>
|
||||
<input type="hidden" name="clientType" value={clientType} />
|
||||
<div>
|
||||
<Header2>Use my OAuth App</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
To use your own OAuth app, check the option below and insert the
|
||||
details.
|
||||
</Paragraph>
|
||||
<Checkbox
|
||||
id="hasCustomClient"
|
||||
label="Use my OAuth App"
|
||||
variant="simple/small"
|
||||
disabled={requiresCustomOAuthApp}
|
||||
onChange={(checked) => setUseMyOAuthApp(checked)}
|
||||
{...conform.input(hasCustomClient, { type: "checkbox" })}
|
||||
defaultChecked={requiresCustomOAuthApp}
|
||||
/>
|
||||
{useMyOAuthApp && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Set the callback url to{" "}
|
||||
<CodeBlock code={callbackUrl} showLineNumbers={false} />
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client ID</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientId, { type: "text" })}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client secret</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientSecret, {
|
||||
type: "password",
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<FormError>{customClientId.error}</FormError>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Header2>Scopes</Header2>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Select the scopes you want to grant to {integration.name} in order
|
||||
for it to access your data. Note: If you try and perform an action
|
||||
in a Job that requires a scope you haven’t granted, that task will
|
||||
fail.
|
||||
</Paragraph>
|
||||
{/* <Header3 className="mb-2">
|
||||
Select from popular scope collections
|
||||
</Header3>
|
||||
<fieldset>
|
||||
<Checkbox
|
||||
id="allScopes"
|
||||
label="Select all"
|
||||
variant="button/small"
|
||||
/>
|
||||
</fieldset> */}
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<Header3>Select {integration.name} scopes</Header3>
|
||||
<Paragraph variant="small" className="text-slate-500">
|
||||
{simplur`${selectedScopes.size} scope[|s] selected`}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search scopes"
|
||||
className="mb-2"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-0.5 overflow-hidden rounded-md">
|
||||
{filteredItems.length === 0 && (
|
||||
<Paragraph variant="small" className="p-4">
|
||||
No scopes match {filterText}. Try a different search query.
|
||||
</Paragraph>
|
||||
)}
|
||||
{authMethod.scopes.map((s) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={s.name}
|
||||
id={s.name}
|
||||
value={s.name}
|
||||
name="scopes"
|
||||
label={s.name}
|
||||
defaultChecked={s.defaultChecked ?? false}
|
||||
badges={s.annotations?.map((a) => a.label)}
|
||||
description={s.description}
|
||||
variant="description"
|
||||
className={cn(
|
||||
filteredItems.find((f) => f.name === s.name) ? "" : "hidden"
|
||||
)}
|
||||
onChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.add(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
} else {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.delete(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Fieldset>
|
||||
|
||||
<div className="flex items-center justify-end gap-x-4">
|
||||
<FormError>{scopes.error}</FormError>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex gap-2"
|
||||
disabled={transition.state !== "idle"}
|
||||
variant="primary/medium"
|
||||
>
|
||||
<>
|
||||
<NamedIcon name={integration.identifier} className={"h-4 w-4"} />
|
||||
Connect to {integration.name}
|
||||
</>
|
||||
</Button>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export function CustomHelp({ name }: { name: string }) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Header1 className="mb-2">
|
||||
You can use any API with requests or an SDK
|
||||
</Header1>
|
||||
<Header2 className="mb-2">How to use an SDK</Header2>
|
||||
<Paragraph spacing>
|
||||
You can call SDK methods from inside the run function, but you should
|
||||
wrap them in a Task to make sure they're resumable.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
Here's an example with the official GitHub SDK
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
new Job(client, {
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
trigger: cronTrigger({
|
||||
cron: "*/5 * * * *", // every 5 minutes
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//wrap an SDK call in io.runTask so it's resumable and displays in logs
|
||||
const repo = await io.runTask(
|
||||
"Get repo",
|
||||
//you can add metadata to the task to improve the display in the logs
|
||||
{ name: "Get repo", icon: "github" },
|
||||
async () => {
|
||||
//this is the regular GitHub SDK
|
||||
const response = await octokit.rest.repos.get({
|
||||
owner: "triggerdotdev",
|
||||
repo: "trigger.dev",
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
`}
|
||||
highlightedRanges={[[9, 22]]}
|
||||
className="mb-4"
|
||||
/>
|
||||
<Header2 className="mb-2">How to use fetch</Header2>
|
||||
<Paragraph spacing>
|
||||
You can use the fetch API to make requests to any API. Or a different
|
||||
request library like axios if you'd prefer. Again wrapping the request
|
||||
in a Task will make sure it's resumable.
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
code={`
|
||||
new Job(client, {
|
||||
id: "scheduled-job-1",
|
||||
name: "Scheduled Job 1",
|
||||
version: "0.1.1",
|
||||
trigger: cronTrigger({
|
||||
cron: "*/5 * * * *", // every 5 minutes
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
//wrap anything in io.runTask so it's resumable and displays in logs
|
||||
const repo = await io.runTask(
|
||||
"Get org",
|
||||
//you can add metadata to the task to improve the display in the logs
|
||||
{ name: "Get org", icon: "github" },
|
||||
async () => {
|
||||
//you can use fetch, axios, or any other library to make requests
|
||||
const response = await fetch('https://api.github.com/orgs/nodejs');
|
||||
return response.json();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
`}
|
||||
highlightedRanges={[[9, 19]]}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import { Api } from "~/services/externalApis/apis";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTrigger,
|
||||
} from "../primitives/Sheet";
|
||||
import { CustomHelp } from "./CustomHelp";
|
||||
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export function NoIntegrationSheet({
|
||||
api,
|
||||
requested,
|
||||
button,
|
||||
}: {
|
||||
api: Api;
|
||||
requested: boolean;
|
||||
button: React.ReactNode;
|
||||
}) {
|
||||
const fetcher = useFetcher();
|
||||
const isLoading = fetcher.state !== "idle";
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger>{button}</SheetTrigger>
|
||||
<SheetContent size="lg">
|
||||
<SheetHeader className="justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<NamedIconInBox name={api.identifier} className="h-9 w-9" />
|
||||
<Header1>{api.name}</Header1>
|
||||
</div>
|
||||
{requested ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckIcon className="h-4 w-4 text-green-500" />
|
||||
<Paragraph variant="small">
|
||||
We'll let you know when the integration is available.
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={`/resources/apivote/${api.identifier}`}
|
||||
>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? "spinner-white" : undefined}
|
||||
>
|
||||
{isLoading
|
||||
? "Saving…"
|
||||
: `I want an integration for ${api.name}`}
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
)}
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
<Callout variant="info">
|
||||
We don’t have an integration for the {api.name} API yet but you can
|
||||
request one by clicking the button above. In the meantime, connect
|
||||
to {api.name} using the methods below.
|
||||
</Callout>
|
||||
<CustomHelp name={api.name} />
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { useFetcher, useLocation, useNavigation } from "@remix-run/react";
|
||||
import cuid from "cuid";
|
||||
import React, { useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { useTextFilter } from "~/hooks/useTextFilter";
|
||||
import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2";
|
||||
import { Integration, Scope } from "~/services/externalApis/types";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Checkbox } from "../primitives/Checkbox";
|
||||
import { Fieldset } from "../primitives/Fieldset";
|
||||
import { FormError } from "../primitives/FormError";
|
||||
import { Header2, Header3 } from "../primitives/Headers";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { InputGroup } from "../primitives/InputGroup";
|
||||
import { Label } from "../primitives/Label";
|
||||
import { NamedIcon, NamedIconInBox } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetTrigger,
|
||||
} from "../primitives/Sheet";
|
||||
|
||||
export type Status = "loading" | "idle";
|
||||
|
||||
export function OAuthConnectSheet({
|
||||
integration,
|
||||
authMethodKey,
|
||||
organizationId,
|
||||
button,
|
||||
className,
|
||||
}: {
|
||||
integration: Integration;
|
||||
authMethodKey: string;
|
||||
organizationId: string;
|
||||
button: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [id] = useState<string>(cuid());
|
||||
const transition = useNavigation();
|
||||
const fetcher = useFetcher();
|
||||
const [
|
||||
form,
|
||||
{ title, scopes, hasCustomClient, customClientId, customClientSecret },
|
||||
] = useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
// Create the schema without any constraint defined
|
||||
schema: createSchema(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const apiAuthmethod = integration.authenticationMethods[authMethodKey];
|
||||
const location = useLocation();
|
||||
|
||||
const options = [
|
||||
{ label: "Your team", value: "DEVELOPER" },
|
||||
{ label: "Your users", value: "EXTERNAL" },
|
||||
];
|
||||
|
||||
const [selectedScopes, setSelectedScopes] = useState<Set<string>>(
|
||||
new Set(
|
||||
apiAuthmethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)
|
||||
)
|
||||
);
|
||||
|
||||
const [useMyOAuthApp, setUseMyOAuthApp] = useState(false);
|
||||
|
||||
const [clientType, setClientType] = useState<"EXTERNAL" | "DEVELOPER">(
|
||||
"DEVELOPER"
|
||||
);
|
||||
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<Scope>({
|
||||
items: apiAuthmethod.scopes,
|
||||
filter: (scope, text) => {
|
||||
if (scope.name.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (
|
||||
scope.description &&
|
||||
scope.description.toLowerCase().includes(text.toLowerCase())
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger className={className}>{button}</SheetTrigger>
|
||||
<SheetContent size="lg">
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={`/resources/connection/${organizationId}/oauth2`}
|
||||
{...form.props}
|
||||
className="flex h-full max-h-full flex-grow flex-col"
|
||||
>
|
||||
<SheetBody>
|
||||
<div className="flex items-center gap-4 border-b border-slate-800 pb-3.5">
|
||||
<NamedIconInBox
|
||||
name={integration.identifier}
|
||||
className="h-9 w-9"
|
||||
/>
|
||||
<div>
|
||||
<Header2>Connect to {integration.name}</Header2>
|
||||
<Paragraph variant="extra-small">
|
||||
{apiAuthmethod.name}{" "}
|
||||
{apiAuthmethod.description &&
|
||||
`– ${apiAuthmethod.description}`}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Fieldset>
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationIdentifier"
|
||||
value={integration.identifier}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="integrationAuthMethod"
|
||||
value={authMethodKey}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="redirectTo"
|
||||
value={location.pathname}
|
||||
/>
|
||||
<InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="large">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
fullWidth
|
||||
{...conform.input(title)}
|
||||
placeholder={`e.g. Personal ${integration.name}`}
|
||||
/>
|
||||
<FormError>{title.error}</FormError>
|
||||
</InputGroup>
|
||||
<Callout variant="info">
|
||||
Coming soon – create connections so you can run Jobs with your
|
||||
users credentials.
|
||||
</Callout>
|
||||
{/* <div>
|
||||
<Header2>Who will connect to this API?</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Select ‘Your team’ if you want create internal Jobs for your
|
||||
own company. Select 'Your users' if you want to create Jobs
|
||||
where your customers authenticate with this API.
|
||||
</Paragraph>
|
||||
<FormSegmentedControl
|
||||
name="clientType"
|
||||
defaultValue={clientType}
|
||||
options={options}
|
||||
onChange={(val) => setClientType(val as any)}
|
||||
/>
|
||||
</div> */}
|
||||
<input type="hidden" name="clientType" value={"DEVELOPER"} />
|
||||
<div>
|
||||
<Header2>Use my OAuth App</Header2>
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
To use your own OAuth app, check the option below and insert
|
||||
the details.
|
||||
</Paragraph>
|
||||
<Checkbox
|
||||
id="hasCustomClient"
|
||||
label="Use my OAuth App"
|
||||
variant="simple/small"
|
||||
defaultChecked={
|
||||
clientType === "EXTERNAL" ? true : useMyOAuthApp
|
||||
}
|
||||
disabled={clientType === "EXTERNAL"}
|
||||
onChange={(checked) => setUseMyOAuthApp(checked)}
|
||||
{...conform.input(hasCustomClient, { type: "checkbox" })}
|
||||
/>
|
||||
{useMyOAuthApp && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Paragraph variant="small" className="mb-2">
|
||||
Set the callback url to{" "}
|
||||
<CodeBlock
|
||||
code={`${origin}/oauth2/callback`}
|
||||
showLineNumbers={false}
|
||||
/>
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client ID</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientId, { type: "text" })}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<Label variant="small">Client secret</Label>
|
||||
<Input
|
||||
fullWidth
|
||||
{...conform.input(customClientSecret, {
|
||||
type: "password",
|
||||
})}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<FormError>{customClientId.error}</FormError>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Header2>Scopes</Header2>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Select the scopes you want to grant to {integration.name} in
|
||||
order for it to access your data. Note: If you try and perform
|
||||
an action in a Job that requires a scope you haven’t granted,
|
||||
that task will fail.
|
||||
</Paragraph>
|
||||
{/* <Header3 className="mb-2">
|
||||
Select from popular scope collections
|
||||
</Header3>
|
||||
<fieldset>
|
||||
<Checkbox
|
||||
id="allScopes"
|
||||
label="Select all"
|
||||
variant="button/small"
|
||||
/>
|
||||
</fieldset> */}
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<Header3>Select {integration.name} scopes</Header3>
|
||||
<Paragraph variant="small" className="text-slate-500">
|
||||
{simplur`${selectedScopes.size} scope[|s] selected`}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search scopes"
|
||||
className="mb-2"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-0.5 overflow-hidden rounded-md">
|
||||
{filteredItems.length === 0 && (
|
||||
<Paragraph variant="small" className="p-4">
|
||||
No scopes match {filterText}. Try a different search
|
||||
query.
|
||||
</Paragraph>
|
||||
)}
|
||||
{apiAuthmethod.scopes.map((s) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={s.name}
|
||||
id={s.name}
|
||||
value={s.name}
|
||||
name="scopes"
|
||||
label={s.name}
|
||||
defaultChecked={s.defaultChecked ?? false}
|
||||
badges={s.annotations?.map((a) => a.label)}
|
||||
description={s.description}
|
||||
variant="description"
|
||||
className={cn(
|
||||
filteredItems.find((f) => f.name === s.name)
|
||||
? ""
|
||||
: "hidden"
|
||||
)}
|
||||
onChange={(isChecked) => {
|
||||
if (isChecked) {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.add(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
} else {
|
||||
setSelectedScopes((selected) => {
|
||||
selected.delete(s.name);
|
||||
return new Set(selected);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Fieldset>
|
||||
</SheetBody>
|
||||
<SheetFooter>
|
||||
<div className="flex items-center justify-end gap-x-4">
|
||||
<FormError>{scopes.error}</FormError>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex gap-2"
|
||||
disabled={transition.state !== "idle"}
|
||||
variant="primary/medium"
|
||||
>
|
||||
<>
|
||||
<NamedIcon
|
||||
name={integration.identifier}
|
||||
className={"h-4 w-4"}
|
||||
/>
|
||||
Connect to {integration.name}
|
||||
</>
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</fetcher.Form>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ApiAuthenticationMethodOAuth2,
|
||||
Integration,
|
||||
} from "~/services/externalApis/types";
|
||||
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
|
||||
import type { ConnectionType } from "@trigger.dev/database";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { ConnectToOAuthForm } from "./ConnectToOAuthForm";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export function SelectOAuthMethod({
|
||||
integration,
|
||||
organizationId,
|
||||
callbackUrl
|
||||
}: {
|
||||
integration: Integration;
|
||||
organizationId: string;
|
||||
callbackUrl: string;
|
||||
}) {
|
||||
const oAuthMethods = Object.entries(integration.authenticationMethods).filter(
|
||||
(a): a is [string, ApiAuthenticationMethodOAuth2] => a[1].type === "oauth2"
|
||||
);
|
||||
|
||||
const [oAuthKey, setOAuthKey] = useState<string | undefined>(
|
||||
oAuthMethods.length === 1 ? oAuthMethods[0][0] : undefined
|
||||
);
|
||||
const [connectionType, setConnectionType] = useState<
|
||||
ConnectionType | undefined
|
||||
>();
|
||||
|
||||
const selectedOAuthMethod = oAuthKey
|
||||
? (integration.authenticationMethods[
|
||||
oAuthKey
|
||||
] as ApiAuthenticationMethodOAuth2)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{oAuthMethods.length > 1 && (
|
||||
<>
|
||||
<Header2 className="mb-2 mt-4">Select an OAuth option</Header2>
|
||||
<RadioGroup
|
||||
name="oauth-method"
|
||||
className="flex gap-2"
|
||||
value={oAuthKey}
|
||||
onValueChange={(v: any) => setOAuthKey(v)}
|
||||
>
|
||||
{oAuthMethods.map(([key, auth]) => (
|
||||
<RadioGroupItem
|
||||
key={key}
|
||||
id={key}
|
||||
value={key}
|
||||
label={auth.name}
|
||||
description={auth.description}
|
||||
variant="description"
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</>
|
||||
)}
|
||||
{selectedOAuthMethod && (
|
||||
<>
|
||||
<Header2 className="mb-2 mt-4">
|
||||
Who is connecting to {integration.name}
|
||||
</Header2>
|
||||
<RadioGroup
|
||||
name="connection-type"
|
||||
className="flex gap-2"
|
||||
value={connectionType}
|
||||
onValueChange={(v: any) => setConnectionType(v as ConnectionType)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id="DEVELOPER"
|
||||
value="DEVELOPER"
|
||||
label="Developers"
|
||||
description={`You will connect using an internal ${integration.name} account.`}
|
||||
variant="description"
|
||||
/>
|
||||
<RadioGroupItem
|
||||
id="EXTERNAL"
|
||||
value="EXTERNAL"
|
||||
label="Your users"
|
||||
description="We will give you OAuth React components so you can connect as your users."
|
||||
variant="description"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</>
|
||||
)}
|
||||
{selectedOAuthMethod &&
|
||||
connectionType &&
|
||||
oAuthKey &&
|
||||
(connectionType === "DEVELOPER" ? (
|
||||
<ConnectToOAuthForm
|
||||
integration={integration}
|
||||
authMethod={selectedOAuthMethod}
|
||||
authMethodKey={oAuthKey}
|
||||
organizationId={organizationId}
|
||||
clientType={connectionType}
|
||||
callbackUrl={callbackUrl}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Header2 className="mb-1 mt-4">User OAuth coming soon</Header2>
|
||||
<Paragraph spacing>
|
||||
End-user OAuth is going to be released soon. If you are interested
|
||||
in being an early beta tester then please{" "}
|
||||
<a
|
||||
href="mailto:founders@trigger.dev"
|
||||
className="text-indigo-500 underline"
|
||||
>
|
||||
message us
|
||||
</a>
|
||||
.
|
||||
</Paragraph>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ConnectionType } from "@trigger.dev/database";
|
||||
|
||||
export function connectionType(type: ConnectionType) {
|
||||
switch (type) {
|
||||
case "DEVELOPER":
|
||||
return "Developer";
|
||||
case "EXTERNAL":
|
||||
return "Your users";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { jobPath } from "~/utils/pathBuilder";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { LabelValueStack } from "../primitives/LabelValueStack";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellChevron,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "../primitives/Table";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { runStatusTitle } from "../runs/RunStatuses";
|
||||
import { ProjectJob, useProject } from "~/hooks/useProject";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
|
||||
export function JobsTable({
|
||||
jobs,
|
||||
noResultsText,
|
||||
}: {
|
||||
jobs: ProjectJob[];
|
||||
noResultsText: string;
|
||||
}) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Job</TableHeaderCell>
|
||||
<TableHeaderCell>ID</TableHeaderCell>
|
||||
<TableHeaderCell>Integrations</TableHeaderCell>
|
||||
<TableHeaderCell>Properties</TableHeaderCell>
|
||||
<TableHeaderCell>Last run</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{jobs.length > 0 ? (
|
||||
jobs.map((job) => {
|
||||
const path = jobPath(organization, project, job);
|
||||
return (
|
||||
<TableRow key={job.id}>
|
||||
<TableCell to={path}>
|
||||
<span className="flex items-center gap-2">
|
||||
<NamedIcon name={job.event.icon} className="h-8 w-8" />
|
||||
<LabelValueStack
|
||||
label={job.title}
|
||||
value={
|
||||
job.dynamic ? (
|
||||
<span className="flex items-center gap-0.5">
|
||||
<NamedIcon name="dynamic" className="h-4 w-4" />{" "}
|
||||
<span className="uppercase">Dynamic:</span>{" "}
|
||||
{job.event.title}
|
||||
</span>
|
||||
) : (
|
||||
job.event.title
|
||||
)
|
||||
}
|
||||
variant="primary"
|
||||
/>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<LabelValueStack
|
||||
label={job.slug}
|
||||
value={`v${job.version}`}
|
||||
variant="primary"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{job.integrations.map((integration) => (
|
||||
<SimpleTooltip
|
||||
key={integration.key}
|
||||
button={
|
||||
<NamedIcon
|
||||
name={integration.icon}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
}
|
||||
content={`${integration.title}: ${integration.key}`}
|
||||
/>
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{job.properties && (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<div className="flex max-w-[200px] items-start justify-start gap-5 truncate">
|
||||
{job.properties.map((property, index) => (
|
||||
<LabelValueStack
|
||||
key={index}
|
||||
label={property.label}
|
||||
value={property.text}
|
||||
className=" last:truncate"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
{job.properties.map((property, index) => (
|
||||
<LabelValueStack
|
||||
key={index}
|
||||
label={property.label}
|
||||
value={property.text}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{job.lastRun ? (
|
||||
<LabelValueStack
|
||||
label={
|
||||
<span className={classForJobStatus(job.lastRun.status)}>
|
||||
{runStatusTitle(job.lastRun.status)}
|
||||
</span>
|
||||
}
|
||||
value={
|
||||
<DateTime
|
||||
date={job.lastRun.createdAt}
|
||||
className={classForJobStatus(job.lastRun.status)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LabelValueStack label={"Never run"} value={"–"} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={6}>
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
{noResultsText}
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function classForJobStatus(status: JobRunStatus) {
|
||||
switch (status) {
|
||||
case "FAILURE":
|
||||
case "TIMED_OUT":
|
||||
case "WAITING_ON_CONNECTIONS":
|
||||
case "PENDING":
|
||||
return "text-rose-500";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ export function PageBody({
|
||||
);
|
||||
}
|
||||
|
||||
export function PageBodyPadding({ children }: { children: React.ReactNode }) {
|
||||
return <div className="p-4">{children}</div>;
|
||||
}
|
||||
|
||||
export function MainCenteredContainer({
|
||||
children,
|
||||
}: {
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { Fragment } from "react";
|
||||
import {
|
||||
useIntegrationClient,
|
||||
useOptionalIntegrationClient,
|
||||
} from "~/hooks/useIntegrationClient";
|
||||
import { useJob, useOptionalJob } from "~/hooks/useJob";
|
||||
import {
|
||||
useOptionalOrganization,
|
||||
useOrganization,
|
||||
} from "~/hooks/useOrganizations";
|
||||
import { useOptionalProject, useProject } from "~/hooks/useProject";
|
||||
import { useOptionalRun, useRun } from "~/hooks/useRun";
|
||||
import {
|
||||
integrationClientConnectionsPath,
|
||||
integrationClientPath,
|
||||
integrationClientScopesPath,
|
||||
jobPath,
|
||||
jobTestPath,
|
||||
projectEnvironmentsPath,
|
||||
projectIntegrationsPath,
|
||||
projectPath,
|
||||
runPath,
|
||||
runDashboardPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
|
||||
import { JobsMenu } from "./JobsMenu";
|
||||
import { BreadcrumbLink } from "./NavBar";
|
||||
import { ProjectsMenu } from "./ProjectsMenu";
|
||||
import { useOptionalRun, useRun } from "~/hooks/useRun";
|
||||
|
||||
export type Breadcrumb = {
|
||||
slug:
|
||||
| "projects"
|
||||
| "jobs"
|
||||
| "integrations"
|
||||
| "integration"
|
||||
| "integration-job"
|
||||
| "integration-connections"
|
||||
| "integration-scopes"
|
||||
| "environments"
|
||||
| "job"
|
||||
| "test"
|
||||
| "runs"
|
||||
| "run";
|
||||
link?: {
|
||||
@@ -49,6 +62,7 @@ export function Breadcrumb() {
|
||||
const project = useOptionalProject();
|
||||
const job = useOptionalJob();
|
||||
const run = useOptionalRun();
|
||||
const client = useOptionalIntegrationClient();
|
||||
|
||||
return (
|
||||
<div className="hidden items-center md:flex">
|
||||
@@ -61,6 +75,7 @@ export function Breadcrumb() {
|
||||
project={project}
|
||||
job={job}
|
||||
run={run}
|
||||
client={client}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
@@ -74,12 +89,14 @@ function BreadcrumbItem({
|
||||
project,
|
||||
job,
|
||||
run,
|
||||
client,
|
||||
}: {
|
||||
breadcrumb: Breadcrumb;
|
||||
organization?: ReturnType<typeof useOrganization>;
|
||||
project?: ReturnType<typeof useProject>;
|
||||
job?: ReturnType<typeof useJob>;
|
||||
run?: ReturnType<typeof useRun>;
|
||||
client?: ReturnType<typeof useIntegrationClient>;
|
||||
}) {
|
||||
switch (breadcrumb.slug) {
|
||||
case "projects":
|
||||
@@ -108,6 +125,45 @@ function BreadcrumbItem({
|
||||
title="Integrations"
|
||||
/>
|
||||
);
|
||||
case "integration":
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={projectIntegrationsPath(organization!, project!)}
|
||||
title="Integrations"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization!, project!, client!)}
|
||||
title={client!.title}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
case "integration-job":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization!, project!, client!)}
|
||||
title={"Jobs"}
|
||||
/>
|
||||
);
|
||||
case "integration-connections":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientConnectionsPath(
|
||||
organization!,
|
||||
project!,
|
||||
client!
|
||||
)}
|
||||
title={"Connections"}
|
||||
/>
|
||||
);
|
||||
case "integration-scopes":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientScopesPath(organization!, project!, client!)}
|
||||
title={"Scopes"}
|
||||
/>
|
||||
);
|
||||
case "job":
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
@@ -119,6 +175,14 @@ function BreadcrumbItem({
|
||||
<JobsMenu key={breadcrumb.slug} />
|
||||
</Fragment>
|
||||
);
|
||||
case "test":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={jobTestPath(organization!, project!, job!)}
|
||||
title="Test"
|
||||
/>
|
||||
);
|
||||
case "runs":
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
@@ -136,7 +200,7 @@ function BreadcrumbItem({
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbLink
|
||||
to={runPath(organization!, project!, job!, run!)}
|
||||
to={runDashboardPath(organization!, project!, job!, run!)}
|
||||
title={`Run #${run!.number}`}
|
||||
/>
|
||||
</Fragment>
|
||||
|
||||
@@ -28,7 +28,11 @@ export function JobsMenu() {
|
||||
<PopoverArrowTrigger isOpen={isOpen}>
|
||||
{currentJob?.title ?? "Select a job"}
|
||||
</PopoverArrowTrigger>
|
||||
<PopoverContent className="w-80 p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-80 overflow-y-auto p-0"
|
||||
align="start"
|
||||
collisionPadding={20}
|
||||
>
|
||||
<PopoverSectionHeader title="Jobs" />
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
{project.jobs.map((job) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Fragment } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { LogoIcon } from "../LogoIcon";
|
||||
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Breadcrumb } from "./Breadcrumb";
|
||||
import { docsRoot } from "~/utils/pathBuilder";
|
||||
|
||||
export function NavBar() {
|
||||
return (
|
||||
@@ -21,20 +21,13 @@ export function NavBar() {
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
<LinkButton
|
||||
to="https://docs.trigger.dev"
|
||||
to={docsRoot()}
|
||||
variant="secondary/small"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
Documentation
|
||||
</LinkButton>
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
data-attr="posthog-feedback-button"
|
||||
LeadingIcon={ChatBubbleLeftRightIcon}
|
||||
shortcut="F"
|
||||
>
|
||||
Send us feedback
|
||||
</Button>
|
||||
<Feedback />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -83,7 +76,7 @@ function MobileDropdownMenu() {
|
||||
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="https://docs.trigger.dev"
|
||||
href={docsRoot()}
|
||||
target="_blank"
|
||||
className="max-w-full"
|
||||
>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { NavLinkButton } from "../primitives/Buttons";
|
||||
import type { IconNames } from "../primitives/NamedIcon";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
|
||||
|
||||
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -26,14 +27,17 @@ export function SideMenuContainer({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
const expandedWith = "14rem";
|
||||
const collapsedWith = "2.81rem";
|
||||
|
||||
const menuVariants = {
|
||||
expanded: {
|
||||
minWidth: "11rem",
|
||||
width: "11rem",
|
||||
minWidth: expandedWith,
|
||||
width: expandedWith,
|
||||
},
|
||||
collapsed: {
|
||||
minWidth: "2.81rem",
|
||||
width: "2.81rem",
|
||||
minWidth: collapsedWith,
|
||||
width: collapsedWith,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,7 +48,8 @@ export function ProjectSideMenu() {
|
||||
|
||||
//we collapse the menu if we're in a job or an integration
|
||||
const job = useOptionalJob();
|
||||
const isCollapsed = job !== undefined;
|
||||
const client = useOptionalIntegrationClient();
|
||||
const isCollapsed = job !== undefined || client !== undefined;
|
||||
|
||||
const jobsActive =
|
||||
job !== undefined ||
|
||||
@@ -75,7 +80,7 @@ export function ProjectSideMenu() {
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Environments"
|
||||
name="Environments & API Keys"
|
||||
icon="environment"
|
||||
to={projectEnvironmentsPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
@@ -89,7 +94,7 @@ export function ProjectSideMenu() {
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Billing"
|
||||
name="Usage & Billing"
|
||||
icon="billing"
|
||||
to={organizationBillingPath(organization)}
|
||||
isCollapsed={isCollapsed}
|
||||
@@ -149,7 +154,7 @@ function SideMenuItem({
|
||||
}}
|
||||
>
|
||||
<motion.span
|
||||
className="pl-1"
|
||||
className="shrink-0 pl-1"
|
||||
animate={isCollapsed ? "collapsed" : "expanded"}
|
||||
variants={itemVariants}
|
||||
initial={isCollapsed ? "collapsed" : "expanded"}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
|
||||
import { Link } from "@remix-run/react";
|
||||
import React from "react";
|
||||
import { Link, LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
|
||||
import React, {
|
||||
ReactComponentElement,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import type { IconNames } from "./NamedIcon";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
|
||||
const variant = {
|
||||
"primary/small": {
|
||||
@@ -12,8 +17,9 @@ const variant = {
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-indigo-600 group-hover:bg-indigo-500/90 group-disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60 justify-self-center",
|
||||
},
|
||||
"secondary/small": {
|
||||
textColor:
|
||||
@@ -21,8 +27,9 @@ const variant = {
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition",
|
||||
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
|
||||
},
|
||||
"tertiary/small": {
|
||||
textColor:
|
||||
@@ -30,8 +37,9 @@ const variant = {
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-transparent group-hover:bg-slate-850 disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition",
|
||||
"ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60",
|
||||
},
|
||||
"danger/small": {
|
||||
textColor:
|
||||
@@ -39,8 +47,9 @@ const variant = {
|
||||
button:
|
||||
"h-6 px-[5px] text-xs bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50 group-disabled:pointer-events-none",
|
||||
icon: "h-3.5",
|
||||
shortcutVariant: "small" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
"ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"primary/medium": {
|
||||
textColor:
|
||||
@@ -48,8 +57,9 @@ const variant = {
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
"ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"secondary/medium": {
|
||||
textColor:
|
||||
@@ -57,8 +67,9 @@ const variant = {
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright transition",
|
||||
"ml-1.5 -mr-0.5 border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright",
|
||||
},
|
||||
"tertiary/medium": {
|
||||
textColor:
|
||||
@@ -66,8 +77,9 @@ const variant = {
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-transparent group-hover:bg-slate-850 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright transition",
|
||||
"ml-1.5 -mr-0.5 border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright",
|
||||
},
|
||||
"danger/medium": {
|
||||
textColor:
|
||||
@@ -75,8 +87,9 @@ const variant = {
|
||||
button:
|
||||
"h-8 px-2 text-sm bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50",
|
||||
icon: "h-4",
|
||||
shortcutVariant: "medium" as const,
|
||||
shortcut:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] ml-1.5 -mr-0.5 border border-bright/40 text-bright group-hover:border-bright/60 transition",
|
||||
"ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
|
||||
},
|
||||
"primary/large": {
|
||||
textColor:
|
||||
@@ -84,6 +97,7 @@ const variant = {
|
||||
button:
|
||||
"h-10 px-2 text-sm font-medium bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50",
|
||||
icon: "h-5",
|
||||
shortcutVariant: undefined,
|
||||
shortcut: undefined,
|
||||
},
|
||||
"secondary/large": {
|
||||
@@ -91,6 +105,7 @@ const variant = {
|
||||
button:
|
||||
"h-10 px-2 text-sm text-dimmed group-hover:text-bright transition font-medium bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50",
|
||||
icon: "h-5",
|
||||
shortcutVariant: undefined,
|
||||
shortcut: undefined,
|
||||
},
|
||||
"menu-item": {
|
||||
@@ -98,6 +113,7 @@ const variant = {
|
||||
button:
|
||||
"h-9 px-[0.475rem] text-sm rounded-sm bg-transparent group-hover:bg-slate-800 transition",
|
||||
icon: "h-5",
|
||||
shortcutVariant: undefined,
|
||||
shortcut: undefined,
|
||||
},
|
||||
};
|
||||
@@ -116,7 +132,7 @@ type ButtonContentPropsType = {
|
||||
fullWidth?: boolean;
|
||||
textAlignLeft?: boolean;
|
||||
className?: string;
|
||||
shortcut?: string;
|
||||
shortcut?: ShortcutDefinition;
|
||||
variant: keyof typeof variant;
|
||||
};
|
||||
|
||||
@@ -132,15 +148,14 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
textAlignLeft,
|
||||
className,
|
||||
} = props;
|
||||
const variation = allVariants.variant[props.variant];
|
||||
|
||||
// Based on the size prop, we'll use the corresponding variant classnames
|
||||
const btnClassName = cn(
|
||||
allVariants.$all,
|
||||
allVariants.variant[props.variant].button
|
||||
);
|
||||
const iconClassName = allVariants.variant[props.variant].icon;
|
||||
const shortcutClassName = allVariants.variant[props.variant].shortcut;
|
||||
const textColorClassName = allVariants.variant[props.variant].textColor;
|
||||
const btnClassName = cn(allVariants.$all, variation.button);
|
||||
const iconClassName = variation.icon;
|
||||
const shortcutClassName = variation.shortcut;
|
||||
const textColorClassName = variation.textColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -181,7 +196,6 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
<span
|
||||
className={cn(
|
||||
"mx-auto grow self-center truncate px-1",
|
||||
|
||||
textColorClassName
|
||||
)}
|
||||
>
|
||||
@@ -211,7 +225,13 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{shortcut && <span className={cn(shortcutClassName)}>{shortcut}</span>}
|
||||
{shortcut && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -223,8 +243,23 @@ type ButtonPropsType = Pick<
|
||||
> &
|
||||
React.ComponentProps<typeof ButtonContent>;
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
({ type, disabled, onClick, ...props }, ref) => {
|
||||
const innerRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement);
|
||||
|
||||
if (props.shortcut) {
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
disabled,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
@@ -233,7 +268,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
onClick={onClick}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
ref={ref}
|
||||
ref={innerRef}
|
||||
form={props.form}
|
||||
>
|
||||
<ButtonContent {...props} />
|
||||
@@ -245,10 +280,23 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
type LinkPropsType = Pick<LinkProps, "to" | "target"> &
|
||||
React.ComponentProps<typeof ButtonContent>;
|
||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
const innerRef = useRef<HTMLAnchorElement>(null);
|
||||
if (props.shortcut) {
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: () => {
|
||||
if (innerRef.current) {
|
||||
innerRef.current.click();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (to.toString().startsWith("http")) {
|
||||
return (
|
||||
<ExtLink
|
||||
href={to.toString()}
|
||||
ref={innerRef}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
>
|
||||
<ButtonContent {...props} />
|
||||
@@ -258,6 +306,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
ref={innerRef}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
>
|
||||
<ButtonContent {...props} />
|
||||
@@ -299,16 +348,19 @@ type ExtLinkProps = JSX.IntrinsicElements["a"] & {
|
||||
href: string;
|
||||
};
|
||||
|
||||
function ExtLink({ className, href, children, ...props }: ExtLinkProps) {
|
||||
return (
|
||||
<a
|
||||
className={cn(className)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const ExtLink = forwardRef<HTMLAnchorElement, ExtLinkProps>(
|
||||
({ className, href, children, ...props }, ref) => {
|
||||
return (
|
||||
<a
|
||||
className={cn(className)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,28 +6,48 @@ import {
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BookOpenIcon,
|
||||
CheckCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
|
||||
const variantClasses = {
|
||||
info: {
|
||||
className: "border-blue-400/20 bg-blue-400/30",
|
||||
icon: <InformationCircleIcon className="h-5 w-5 text-blue-400" />,
|
||||
icon: <InformationCircleIcon className="h-5 w-5 shrink-0 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
},
|
||||
warning: {
|
||||
className: "border-yellow-400/20 bg-yellow-400/30",
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5 text-yellow-400" />,
|
||||
icon: (
|
||||
<ExclamationCircleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
|
||||
),
|
||||
text: "text-yellow-200",
|
||||
},
|
||||
error: {
|
||||
className: "border-[hsl(347,77%,50%)]/30 bg-[hsl(347,77%,50%)]/20",
|
||||
icon: <ExclamationTriangleIcon className="h-5 w-5 text-rose-400" />,
|
||||
icon: (
|
||||
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-rose-400" />
|
||||
),
|
||||
text: "text-rose-200",
|
||||
},
|
||||
idea: {
|
||||
className: "border-green-400/20 bg-green-400/30",
|
||||
icon: <LightBulbIcon className="h-5 w-5 text-green-400" />,
|
||||
icon: <LightBulbIcon className="h-5 w-5 shrink-0 text-green-400" />,
|
||||
text: "text-green-200",
|
||||
},
|
||||
success: {
|
||||
className: "border-green-400/20 bg-green-400/30",
|
||||
icon: <CheckCircleIcon className="h-5 w-5 shrink-0 text-green-400" />,
|
||||
text: "text-green-200",
|
||||
},
|
||||
docs: {
|
||||
className:
|
||||
"border-blue-400/20 bg-blue-400/30 transition hover:bg-blue-400/40",
|
||||
icon: <BookOpenIcon className="h-5 w-5 shrink-0 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function Callout({
|
||||
@@ -35,33 +55,60 @@ export function Callout({
|
||||
className,
|
||||
icon,
|
||||
variant,
|
||||
href,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
variant: keyof typeof variantClasses;
|
||||
href?: string;
|
||||
}) {
|
||||
const variantDefinition = variantClasses[variant];
|
||||
|
||||
if (variant === "docs") {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
<ArrowTopRightOnSquareIcon className="h-5 w-5 text-blue-400" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-full gap-4 rounded-md border py-3 pl-3 pr-4 shadow-md backdrop-blur-sm`,
|
||||
`flex w-full items-start gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-start gap-2.5">
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const variants = {
|
||||
},
|
||||
description: {
|
||||
button: "w-full py-2 pl-3 pr-4 checked:hover:bg-slate-850 transition",
|
||||
label: "text-bright font-mono",
|
||||
label: "text-bright font-semibold",
|
||||
description: "text-dimmed",
|
||||
inputPosition: "mt-1",
|
||||
isChecked: "bg-slate-850",
|
||||
@@ -59,7 +59,6 @@ export type CheckboxProps = Omit<
|
||||
variant?: keyof typeof variants;
|
||||
label?: string;
|
||||
description?: string;
|
||||
badge?: string;
|
||||
badges?: string[];
|
||||
className?: string;
|
||||
onChange?: (isChecked: boolean) => void;
|
||||
@@ -76,7 +75,6 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
label,
|
||||
description,
|
||||
defaultChecked,
|
||||
badge,
|
||||
badges,
|
||||
disabled,
|
||||
className,
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Button } from "./Buttons";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { CheckIcon } from "@heroicons/react/20/solid";
|
||||
import { IconNames, NamedIcon } from "./NamedIcon";
|
||||
|
||||
const variations = {
|
||||
"primary/small": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-800 bg-slate-850 text-xs transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "primary/small" as const,
|
||||
button: "rounded-l-none min-w-[3.1rem]",
|
||||
iconSize: "h-3 w-3",
|
||||
iconPadding: "pl-1",
|
||||
},
|
||||
"secondary/small": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-750 bg-slate-850 text-xs transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "secondary/small" as const,
|
||||
button: "rounded-l-none border-l border-slate-750 min-w-[3.1rem]",
|
||||
iconSize: "h-3 w-3",
|
||||
iconPadding: "pl-1",
|
||||
},
|
||||
"tertiary/small": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-850 bg-transparent text-xs transition hover:border-slate-800 hover:bg-slate-950 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-transparent border-0 text-xs px-2 w-auto rounded-l h-6 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "tertiary/small" as const,
|
||||
button: "rounded-l-none border-l border-slate-850 min-w-[3.1rem]",
|
||||
iconSize: "h-3 w-3",
|
||||
iconPadding: "pl-1",
|
||||
},
|
||||
"primary/medium": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-800 bg-slate-850 text-sm transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "primary/medium" as const,
|
||||
button: "rounded-l-none min-w-[4rem]",
|
||||
iconSize: "h-4 w-4",
|
||||
iconPadding: "pl-2",
|
||||
},
|
||||
"secondary/medium": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-750 bg-slate-850 text-sm transition hover:border-slate-750 hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:border-transparent focus:outline-none focus:ring-0 focus:ring-transparent",
|
||||
input:
|
||||
"bg-slate-850 border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "secondary/medium" as const,
|
||||
button: "rounded-l-none border-l border-slate-750 min-w-[4rem]",
|
||||
iconSize: "h-4 w-4",
|
||||
iconPadding: "pl-2",
|
||||
},
|
||||
"tertiary/medium": {
|
||||
container:
|
||||
"flex items-center text-dimmed font-mono rounded border border-slate-850 bg-transparent text-sm transition hover:border-slate-800 hover:bg-slate-950 focus:border-4 focus:border-solid focus:border-l-transparent",
|
||||
input:
|
||||
"bg-transparent border-0 text-sm px-3 w-auto rounded-l h-8 leading-6 focus:ring-transparent",
|
||||
buttonVariant: "tertiary/medium" as const,
|
||||
button: "rounded-l-none border-l border-slate-850 min-w-[4rem]",
|
||||
iconSize: "h-4 w-4",
|
||||
iconPadding: "pl-2",
|
||||
},
|
||||
};
|
||||
|
||||
type ClipboardFieldProps = {
|
||||
value: string;
|
||||
secure?: boolean;
|
||||
variant: keyof typeof variations;
|
||||
className?: string;
|
||||
icon?: IconNames | React.ReactNode;
|
||||
};
|
||||
|
||||
export function ClipboardField({
|
||||
value,
|
||||
secure = false,
|
||||
variant,
|
||||
className,
|
||||
icon,
|
||||
}: ClipboardFieldProps) {
|
||||
const [isSecure, setIsSecure] = useState(secure);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1500);
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const { container, input, buttonVariant, button } = variations[variant];
|
||||
const iconClassName = variations[variant].iconSize;
|
||||
const iconPosition = variations[variant].iconPadding;
|
||||
const inputIcon = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<div className={cn(container, className)}>
|
||||
{icon && (
|
||||
<div
|
||||
onClick={() => inputIcon.current && inputIcon.current.focus()}
|
||||
className={cn(iconPosition, "flex items-center")}
|
||||
>
|
||||
{typeof icon === "string" ? (
|
||||
<NamedIcon name={icon} className={iconClassName} />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
ref={inputIcon}
|
||||
value={isSecure ? "•".repeat(value.length) : value}
|
||||
readOnly={true}
|
||||
className={cn("shrink grow select-all overflow-x-auto", input)}
|
||||
size={value.length}
|
||||
onFocus={(e) => {
|
||||
if (secure) {
|
||||
setIsSecure((i) => false);
|
||||
}
|
||||
e.currentTarget.select();
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (secure) {
|
||||
setIsSecure((i) => true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
onClick={copy}
|
||||
className={cn("shrink-0 grow-0", button)}
|
||||
>
|
||||
{copied ? <CheckIcon className="h-4 w-4 text-green-500" /> : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function FormButtons({
|
||||
cancelButton,
|
||||
confirmButton,
|
||||
className,
|
||||
}: {
|
||||
cancelButton?: React.ReactNode;
|
||||
confirmButton: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className={cn("flex w-full items-center justify-between", className)}>
|
||||
{cancelButton ? cancelButton : <div />} {confirmButton}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,9 +22,13 @@ const HelpContext = React.createContext<HelpContextValue>({
|
||||
type HelpProps = {
|
||||
defaultOpen?: boolean;
|
||||
allowDismissing?: boolean;
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode | ((open: boolean) => React.ReactNode);
|
||||
};
|
||||
|
||||
function useHelp() {
|
||||
return React.useContext(HelpContext);
|
||||
}
|
||||
|
||||
export function Help({
|
||||
defaultOpen,
|
||||
allowDismissing = true,
|
||||
@@ -34,13 +38,13 @@ export function Help({
|
||||
|
||||
return (
|
||||
<HelpContext.Provider value={{ open, setOpen, allowDismissing }}>
|
||||
{children}
|
||||
{typeof children === "function" ? children(open) : children}
|
||||
</HelpContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function HelpTrigger({ title }: { title: string }) {
|
||||
const { open, setOpen } = React.useContext(HelpContext);
|
||||
const { open, setOpen } = useHelp();
|
||||
|
||||
return open ? (
|
||||
<></>
|
||||
@@ -65,7 +69,7 @@ export function HelpContent({
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { open, setOpen, allowDismissing } = React.useContext(HelpContext);
|
||||
const { open, setOpen, allowDismissing } = useHelp();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -89,7 +93,7 @@ export function HelpContent({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grow rounded border border-slate-850 bg-midnight-850 bg-contain bg-left-top bg-no-repeat p-4"
|
||||
className="grow rounded border border-slate-850 bg-midnight-850 bg-contain bg-left-top bg-no-repeat p-6"
|
||||
style={{
|
||||
backgroundImage: `url(${gradientPath})`,
|
||||
}}
|
||||
|
||||
@@ -40,12 +40,13 @@ export function LabelValueStack({
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-baseline",
|
||||
layout === "vertical" ? "flex-col" : "gap-1",
|
||||
layout === "vertical" && "flex-col",
|
||||
variant === "primary" ? "gap-x-1 gap-y-0" : "gap-x-1 gap-y-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Paragraph variant={variation.label}>{label}</Paragraph>
|
||||
<div>
|
||||
<>
|
||||
{href ? (
|
||||
<SimpleTooltip
|
||||
side="bottom"
|
||||
@@ -53,7 +54,7 @@ export function LabelValueStack({
|
||||
<Paragraph variant={variation.value}>
|
||||
<a
|
||||
href={href}
|
||||
className=" underline underline-offset-2"
|
||||
className="underline underline-offset-2"
|
||||
target="_blank"
|
||||
>
|
||||
{value}
|
||||
@@ -66,7 +67,7 @@ export function LabelValueStack({
|
||||
) : (
|
||||
<Paragraph variant={variation.value}>{value}</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@ import {
|
||||
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowPathIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BeakerIcon,
|
||||
BellAlertIcon,
|
||||
BoltIcon,
|
||||
BookOpenIcon,
|
||||
BuildingOffice2Icon,
|
||||
CalendarDaysIcon,
|
||||
@@ -20,6 +23,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
ClockIcon,
|
||||
CloudIcon,
|
||||
CodeBracketSquareIcon,
|
||||
Cog8ToothIcon,
|
||||
CreditCardIcon,
|
||||
@@ -49,6 +53,8 @@ import { WebhookIcon } from "~/assets/icons/WebhookIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { LogoIcon } from "../LogoIcon";
|
||||
import { Spinner } from "./Spinner";
|
||||
import { HourglassIcon } from "lucide-react";
|
||||
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
|
||||
|
||||
const icons = {
|
||||
account: (className: string) => (
|
||||
@@ -60,6 +66,9 @@ const icons = {
|
||||
"arrow-left": (className: string) => (
|
||||
<ArrowLeftIcon className={cn("text-white", className)} />
|
||||
),
|
||||
background: (className: string) => (
|
||||
<CloudIcon className={cn("text-sky-400", className)} />
|
||||
),
|
||||
beaker: (className: string) => (
|
||||
<BeakerIcon className={cn("text-purple-500", className)} />
|
||||
),
|
||||
@@ -84,6 +93,9 @@ const icons = {
|
||||
"chevron-right": (className: string) => (
|
||||
<ChevronRightIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
countdown: (className: string) => (
|
||||
<HourglassIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
clock: (className: string) => (
|
||||
<ClockIcon className={cn("text-cyan-500", className)} />
|
||||
),
|
||||
@@ -96,9 +108,15 @@ const icons = {
|
||||
docs: (className: string) => (
|
||||
<BookOpenIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
dynamic: (className: string) => (
|
||||
<DynamicTriggerIcon className={cn("text-cyan-500", className)} />
|
||||
),
|
||||
error: (className: string) => (
|
||||
<ExclamationCircleIcon className={cn("text-rose-500", className)} />
|
||||
),
|
||||
"external-link": (className: string) => (
|
||||
<ArrowTopRightOnSquareIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
flag: (className: string) => (
|
||||
<FlagIcon className={cn("text-sky-500", className)} />
|
||||
),
|
||||
@@ -144,12 +162,10 @@ const icons = {
|
||||
log: (className: string) => (
|
||||
<ChatBubbleLeftEllipsisIcon className={cn("text-slate-400", className)} />
|
||||
),
|
||||
"logo-icon": (className: string) => <LogoIcon className={cn(className)} />,
|
||||
organization: (className: string) => (
|
||||
<BuildingOffice2Icon className={cn("text-fuchsia-600", className)} />
|
||||
),
|
||||
search: (className: string) => (
|
||||
<MagnifyingGlassIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
plus: (className: string) => (
|
||||
<PlusIcon className={cn("text-green-600", className)} />
|
||||
),
|
||||
@@ -159,6 +175,15 @@ const icons = {
|
||||
property: (className: string) => (
|
||||
<Cog8ToothIcon className={cn("text-slate-600", className)} />
|
||||
),
|
||||
refresh: (className: string) => (
|
||||
<ArrowPathIcon className={cn("text-bright", className)} />
|
||||
),
|
||||
search: (className: string) => (
|
||||
<MagnifyingGlassIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
settings: (className: string) => (
|
||||
<Cog8ToothIcon className={cn("text-slate-600", className)} />
|
||||
),
|
||||
spinner: (className: string) => (
|
||||
<Spinner className={className} color="blue" />
|
||||
),
|
||||
@@ -171,7 +196,9 @@ const icons = {
|
||||
team: (className: string) => (
|
||||
<UserGroupIcon className={cn("text-blue-500", className)} />
|
||||
),
|
||||
"logo-icon": (className: string) => <LogoIcon className={cn(className)} />,
|
||||
trigger: (className: string) => (
|
||||
<BoltIcon className={cn("text-orange-500", className)} />
|
||||
),
|
||||
user: (className: string) => (
|
||||
<UserIcon className={cn("text-blue-600", className)} />
|
||||
),
|
||||
@@ -224,6 +251,9 @@ export function NamedIcon({
|
||||
name={name}
|
||||
className={"h-full w-full p-[7%]"}
|
||||
variant="light"
|
||||
style={{
|
||||
shapeRendering: "geometricPrecision",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useContext } from "react";
|
||||
import { createContext } from "react";
|
||||
|
||||
export type OperatingSystemPlatform = "mac" | "windows";
|
||||
|
||||
type OperatingSystemContext = {
|
||||
platform: OperatingSystemPlatform;
|
||||
};
|
||||
|
||||
type OperatingSystemContextProviderProps = {
|
||||
platform: OperatingSystemPlatform;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Context = createContext<OperatingSystemContext | null>(null);
|
||||
|
||||
export const OperatingSystemContextProvider = ({
|
||||
platform,
|
||||
children,
|
||||
}: OperatingSystemContextProviderProps) => {
|
||||
return <Context.Provider value={{ platform }}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
const throwIfNoProvider = () => {
|
||||
throw new Error(
|
||||
"Please wrap your application in an OperatingSystemContextProvider."
|
||||
);
|
||||
};
|
||||
|
||||
export const useOperatingSystem = () => {
|
||||
const { platform } = useContext(Context) ?? throwIfNoProvider();
|
||||
return { platform };
|
||||
};
|
||||
@@ -65,7 +65,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
|
||||
}
|
||||
|
||||
export function PageButtons({ children }: WithChildren) {
|
||||
return <div className="flex gap-2">{children}</div>;
|
||||
return <div className="flex items-center gap-3">{children}</div>;
|
||||
}
|
||||
|
||||
export function PageDescription({ children }: WithChildren) {
|
||||
@@ -102,7 +102,7 @@ export function PageInfoProperty({
|
||||
value,
|
||||
}: {
|
||||
icon?: string | React.ReactNode;
|
||||
label: string;
|
||||
label?: string;
|
||||
value: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
@@ -112,12 +112,14 @@ export function PageInfoProperty({
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<Paragraph
|
||||
variant="extra-small/caps"
|
||||
className="mt-0.5 whitespace-nowrap"
|
||||
>
|
||||
{label}:
|
||||
</Paragraph>
|
||||
{label && (
|
||||
<Paragraph
|
||||
variant="extra-small/caps"
|
||||
className="mt-0.5 whitespace-nowrap"
|
||||
>
|
||||
{label}:
|
||||
</Paragraph>
|
||||
)}
|
||||
{typeof value === "string" || typeof value === "number" ? (
|
||||
<Paragraph variant="small">{value}</Paragraph>
|
||||
) : (
|
||||
|
||||
@@ -1,24 +1,62 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const paragraphVariants = {
|
||||
base: "font-sans text-base font-normal text-dimmed",
|
||||
"base/bright": "font-sans text-base font-normal text-bright",
|
||||
small: "font-sans text-sm font-normal text-dimmed",
|
||||
"small/bright": "font-sans text-sm font-normal text-bright",
|
||||
"extra-small": "font-sans text-xs font-normal text-dimmed",
|
||||
"extra-small/bright": "font-sans text-xs font-normal text-bright",
|
||||
"extra-small/mono": "font-mono text-xs font-normal text-dimmed",
|
||||
"extra-small/bright/mono": "font-mono text-xs text-bright",
|
||||
"extra-small/caps":
|
||||
"font-sans text-xs uppercase tracking-wider font-normal text-dimmed",
|
||||
"extra-small/bright/caps":
|
||||
"font-sans text-xs uppercase tracking-wider font-normal text-bright",
|
||||
"extra-extra-small": "font-sans text-xxs font-normal text-dimmed",
|
||||
"extra-extra-small/bright": "font-sans text-xxs font-normal text-bright",
|
||||
"extra-extra-small/caps":
|
||||
"font-sans text-xxs uppercase tracking-wider font-normal text-dimmed",
|
||||
"extra-extra-small/bright/caps":
|
||||
"font-sans text-xxs uppercase tracking-wider font-normal text-bright",
|
||||
base: {
|
||||
text: "font-sans text-base font-normal text-dimmed",
|
||||
spacing: "mb-3",
|
||||
},
|
||||
"base/bright": {
|
||||
text: "font-sans text-base font-normal text-bright",
|
||||
spacing: "mb-3",
|
||||
},
|
||||
small: {
|
||||
text: "font-sans text-sm font-normal text-dimmed",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
"small/bright": {
|
||||
text: "font-sans text-sm font-normal text-bright",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
"extra-small": {
|
||||
text: "font-sans text-xs font-normal text-dimmed",
|
||||
spacing: "mb-1.5",
|
||||
},
|
||||
"extra-small/bright": {
|
||||
text: "font-sans text-xs font-normal text-bright",
|
||||
spacing: "mb-1.5",
|
||||
},
|
||||
"extra-small/mono": {
|
||||
text: "font-mono text-xs font-normal text-dimmed",
|
||||
spacing: "mb-1.5",
|
||||
},
|
||||
"extra-small/bright/mono": {
|
||||
text: "font-mono text-xs text-bright",
|
||||
spacing: "mb-1.5",
|
||||
},
|
||||
"extra-small/caps": {
|
||||
text: "font-sans text-xs uppercase tracking-wider font-normal text-dimmed",
|
||||
spacing: "mb-1.5",
|
||||
},
|
||||
"extra-small/bright/caps": {
|
||||
text: "font-sans text-xs uppercase tracking-wider font-normal text-bright",
|
||||
spacing: "mb-1.5",
|
||||
},
|
||||
"extra-extra-small": {
|
||||
text: "font-sans text-xxs font-normal text-dimmed",
|
||||
spacing: "mb-1",
|
||||
},
|
||||
"extra-extra-small/bright": {
|
||||
text: "font-sans text-xxs font-normal text-bright",
|
||||
spacing: "mb-1",
|
||||
},
|
||||
"extra-extra-small/caps": {
|
||||
text: "font-sans text-xxs uppercase tracking-wider font-normal text-dimmed",
|
||||
spacing: "mb-1",
|
||||
},
|
||||
"extra-extra-small/bright/caps": {
|
||||
text: "font-sans text-xxs uppercase tracking-wider font-normal text-bright",
|
||||
spacing: "mb-1",
|
||||
},
|
||||
};
|
||||
|
||||
export type ParagraphVariant = keyof typeof paragraphVariants;
|
||||
@@ -26,17 +64,26 @@ export type ParagraphVariant = keyof typeof paragraphVariants;
|
||||
type ParagraphProps = {
|
||||
variant?: ParagraphVariant;
|
||||
className?: string;
|
||||
spacing?: boolean;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLAttributes<HTMLParagraphElement>;
|
||||
|
||||
export function Paragraph({
|
||||
variant = "base",
|
||||
className,
|
||||
spacing = false,
|
||||
children,
|
||||
...props
|
||||
}: ParagraphProps) {
|
||||
return (
|
||||
<p className={cn(paragraphVariants[variant], className)} {...props}>
|
||||
<p
|
||||
className={cn(
|
||||
paragraphVariants[variant].text,
|
||||
spacing === true && paragraphVariants[variant].spacing,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Badge } from "./Badge";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const variants = {
|
||||
"simple/small": {
|
||||
button: "w-fit pr-4 data-[disabled]:opacity-70",
|
||||
label: "text-sm text-bright mt-0.5 select-none",
|
||||
description: "text-dimmed",
|
||||
inputPosition: "mt-1",
|
||||
},
|
||||
simple: {
|
||||
button: "w-fit pr-4 data-[disabled]:opacity-70",
|
||||
label: "text-bright select-none",
|
||||
description: "text-dimmed",
|
||||
inputPosition: "mt-1",
|
||||
},
|
||||
"button/small": {
|
||||
button:
|
||||
"flex items-center w-fit h-8 pl-2 pr-3 rounded border border-slate-800 hover:bg-slate-850 hover:border-slate-750 transition data-[disabled]:opacity-70 data-[disabled]:hover:bg-transparent data-[state=checked]:bg-slate-850",
|
||||
label: "text-sm text-bright select-none",
|
||||
description: "text-dimmed",
|
||||
inputPosition: "mt-0",
|
||||
},
|
||||
button: {
|
||||
button:
|
||||
"w-fit py-2 pl-3 pr-4 rounded border border-slate-800 hover:bg-slate-850 hover:border-slate-750 transition data-[state=checked]:bg-slate-850 data-[disabled]:opacity-70",
|
||||
label: "text-bright select-none",
|
||||
description: "text-dimmed",
|
||||
inputPosition: "mt-1",
|
||||
},
|
||||
description: {
|
||||
button:
|
||||
"w-full py-2 pl-2 pr-3 hover:bg-slate-850 transition data-[disabled]:opacity-70 data-[state=checked]:bg-slate-850 border-slate-800 border rounded-sm",
|
||||
label: "text-bright font-semibold -mt-1 text-left",
|
||||
description: "text-dimmed -mt-0.5 text-left",
|
||||
inputPosition: "mt-0",
|
||||
},
|
||||
};
|
||||
|
||||
export const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root className={className} {...props} ref={ref} />
|
||||
);
|
||||
});
|
||||
|
||||
type RadioGroupItemProps = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
|
||||
"onChange"
|
||||
> & {
|
||||
variant?: keyof typeof variants;
|
||||
label?: string;
|
||||
description?: string;
|
||||
badges?: string[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
RadioGroupItemProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
variant = "simple",
|
||||
label,
|
||||
description,
|
||||
badges,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const variation = variants[variant];
|
||||
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex cursor-pointer items-start gap-x-2 transition",
|
||||
variation.button,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-sm border border-slate-700 ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
variation.inputPosition
|
||||
)}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex h-full w-full items-center justify-center bg-indigo-700">
|
||||
<Circle className="h-1.5 w-1.5 fill-white text-white" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
className={cn("cursor-pointer", variation.label)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
{badges && (
|
||||
<span className="-mr-2 flex gap-x-1.5">
|
||||
{badges.map((badge) => (
|
||||
<Badge key={badge}>{badge}</Badge>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{variant === "description" && (
|
||||
<Paragraph
|
||||
variant="small"
|
||||
className={cn("mt-0.5", variation.description)}
|
||||
>
|
||||
{description}
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -104,7 +104,7 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50 relative my-0.5 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition first-of-type:my-1 last-of-type:my-1 hover:bg-slate-750 focus:bg-slate-750/50",
|
||||
"relative my-0.5 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition first-of-type:my-1 last-of-type:my-1 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-slate-750 focus:bg-slate-750/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
@@ -159,15 +158,17 @@ const SheetContent = React.forwardRef<
|
||||
className={cn(sheetVariants({ position, size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="grid h-full grid-rows-[2.75rem_1fr]">
|
||||
<div className="grid max-h-full grid-rows-[2.75rem_1fr] overflow-hidden">
|
||||
<div className="flex items-center gap-2 border-b border-slate-800 p-2">
|
||||
<SheetPrimitive.Close className="rounded-sm p-1 transition hover:bg-slate-800 disabled:pointer-events-none">
|
||||
<NamedIcon name="close" className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
<ShortcutKey shortcut="esc" variant="medium" />
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
|
||||
</div>
|
||||
<div className="flex max-h-full flex-col overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
<div className="overflow-hidden">{children}</div>
|
||||
</div>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
@@ -180,7 +181,20 @@ export const SheetBody = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grow overflow-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
|
||||
"overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-4 flex shrink-0 items-center gap-4 border-b border-slate-800 py-3.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -197,4 +211,4 @@ export const SheetFooter = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
export { Sheet, SheetTrigger, SheetContent };
|
||||
export { Sheet, SheetContent, SheetTrigger };
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Fragment } from "react";
|
||||
import { Modifier, ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useOperatingSystem } from "./OperatingSystemProvider";
|
||||
|
||||
const variants = {
|
||||
small:
|
||||
"text-xxs font-medium py-0.25 min-w-[14px] rounded-[2px] px-0.5 border border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright transition",
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition uppercase",
|
||||
medium:
|
||||
"text-[0.6rem] leading-[1.13rem] min-w-[20px] px-1 rounded-[3px] border border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright transition",
|
||||
"text-[0.75rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60 transition uppercase",
|
||||
};
|
||||
|
||||
export type ShortcutKeyVariant = keyof typeof variants;
|
||||
|
||||
type ShortcutKeyProps = {
|
||||
shortcut: string;
|
||||
shortcut: ShortcutDefinition;
|
||||
variant: ShortcutKeyVariant;
|
||||
className?: string;
|
||||
};
|
||||
@@ -20,5 +23,32 @@ export function ShortcutKey({
|
||||
variant,
|
||||
className,
|
||||
}: ShortcutKeyProps) {
|
||||
return <span className={cn(variants[variant], className)}>{shortcut}</span>;
|
||||
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;
|
||||
|
||||
return (
|
||||
<span className={cn(variants[variant], className)}>
|
||||
{modifiers.map((k) => (
|
||||
<Fragment key={k}>{modifierString(k, isMac)}</Fragment>
|
||||
))}
|
||||
{character}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function modifierString(modifier: Modifier, isMac: boolean) {
|
||||
switch (modifier) {
|
||||
case "alt":
|
||||
return isMac ? "⌥" : "Alt+";
|
||||
case "ctrl":
|
||||
return isMac ? "⌃" : "Ctrl+";
|
||||
case "meta":
|
||||
return isMac ? "⌘" : "⊞";
|
||||
case "shift":
|
||||
return isMac ? "⇧" : "Shift+";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
export function TutorialStep({
|
||||
import { Header2 } from "./Headers";
|
||||
|
||||
export function StepNumber({
|
||||
stepNumber,
|
||||
drawLine,
|
||||
active = false,
|
||||
complete = false,
|
||||
title,
|
||||
}: {
|
||||
stepNumber?: string;
|
||||
drawLine?: boolean;
|
||||
active?: boolean;
|
||||
complete?: boolean;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mr-3 flex flex-col items-center justify-center">
|
||||
<div className="mr-3">
|
||||
{active ? (
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded bg-green-600 py-1 text-sm font-semibold text-slate-900 shadow">
|
||||
{stepNumber}
|
||||
</span>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded bg-green-600 py-1 text-xs font-semibold text-slate-900 shadow">
|
||||
{stepNumber}
|
||||
</span>
|
||||
<Header2>{title}</Header2>
|
||||
</div>
|
||||
) : (
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded border border-slate-700 bg-slate-800 py-1 text-sm font-semibold text-green-400 shadow">
|
||||
{complete ? "✓" : stepNumber}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{drawLine ? (
|
||||
<div className="h-full border-l border-slate-700"></div>
|
||||
) : (
|
||||
<div className="h-full"></div>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded border border-slate-700 bg-slate-800 py-1 text-xs font-semibold text-dimmed shadow">
|
||||
{complete ? "✓" : stepNumber}
|
||||
</span>
|
||||
<Header2>{title}</Header2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variations = {
|
||||
large: {
|
||||
container: "flex items-center gap-x-2 rounded-md hover:bg-slate-850 p-2",
|
||||
root: "h-6 w-11",
|
||||
thumb:
|
||||
"h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-sm text-slate-400 group-hover:text-slate-200",
|
||||
},
|
||||
small: {
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
type SwitchProps = React.ComponentPropsWithoutRef<
|
||||
typeof SwitchPrimitives.Root
|
||||
> & {
|
||||
label?: React.ReactNode;
|
||||
variant: keyof typeof variations;
|
||||
};
|
||||
|
||||
export const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
SwitchProps
|
||||
>(({ className, variant, label, ...props }, ref) => {
|
||||
const { container, root, thumb, text } = variations[variant];
|
||||
|
||||
return (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn("group", container, className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{label ? (
|
||||
<label className={cn("text", text)}>
|
||||
{typeof label === "string" ? <span>{label}</span> : label}
|
||||
</label>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-background group-disabled:cursor-not-allowed group-disabled:opacity-50 group-data-[state=checked]:bg-primary group-data-[state=unchecked]:bg-slate-700 focus-visible:outline-none",
|
||||
root
|
||||
)}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
thumb,
|
||||
"pointer-events-none block rounded-full bg-slate-200 shadow-lg ring-0 transition-transform"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</SwitchPrimitives.Root>
|
||||
);
|
||||
});
|
||||
@@ -86,6 +86,7 @@ type TableCellBasicProps = {
|
||||
className?: string;
|
||||
alignment?: "left" | "center" | "right";
|
||||
children: ReactNode;
|
||||
colSpan?: number;
|
||||
};
|
||||
|
||||
type TableHeaderCellProps = TableCellBasicProps & {
|
||||
@@ -95,38 +96,11 @@ type TableHeaderCellProps = TableCellBasicProps & {
|
||||
export const TableHeaderCell = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
TableHeaderCellProps
|
||||
>(({ className, alignment = "left", children, hiddenLabel = false }, ref) => {
|
||||
let alignmentClassName = "text-left";
|
||||
switch (alignment) {
|
||||
case "center":
|
||||
alignmentClassName = "text-center";
|
||||
break;
|
||||
case "right":
|
||||
alignmentClassName = "text-right";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<th
|
||||
ref={ref}
|
||||
scope="col"
|
||||
className={cn(
|
||||
"px-4 py-3 align-middle text-xs font-semibold uppercase text-slate-400",
|
||||
alignmentClassName,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{hiddenLabel ? <span className="sr-only">{children}</span> : children}
|
||||
</th>
|
||||
);
|
||||
});
|
||||
|
||||
type TableCellProps = TableCellBasicProps & {
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
({ className, alignment = "left", children, to }, ref) => {
|
||||
>(
|
||||
(
|
||||
{ className, alignment = "left", children, colSpan, hiddenLabel = false },
|
||||
ref
|
||||
) => {
|
||||
let alignmentClassName = "text-left";
|
||||
switch (alignment) {
|
||||
case "center":
|
||||
@@ -137,30 +111,68 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<th
|
||||
ref={ref}
|
||||
scope="col"
|
||||
className={cn(
|
||||
"px-4 py-3 align-middle text-xs font-semibold uppercase text-slate-400",
|
||||
alignmentClassName,
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
>
|
||||
{hiddenLabel ? <span className="sr-only">{children}</span> : children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TableCellProps = TableCellBasicProps & {
|
||||
to?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
};
|
||||
|
||||
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
({ className, alignment = "left", children, colSpan, to, onClick }, ref) => {
|
||||
let alignmentClassName = "text-left";
|
||||
switch (alignment) {
|
||||
case "center":
|
||||
alignmentClassName = "text-center";
|
||||
break;
|
||||
case "right":
|
||||
alignmentClassName = "text-right";
|
||||
break;
|
||||
}
|
||||
|
||||
const flexClasses = cn(
|
||||
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-slate-400",
|
||||
alignment === "left"
|
||||
? "justify-start text-left"
|
||||
: alignment === "center"
|
||||
? "justify-center text-center"
|
||||
: "justify-end text-right"
|
||||
);
|
||||
|
||||
return (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-xs text-slate-400 transition group-hover:bg-slate-850/50",
|
||||
to ? "cursor-pointer" : "px-4 py-3 align-middle",
|
||||
!to && alignmentClassName,
|
||||
to || onClick ? "cursor-pointer" : "px-4 py-3 align-middle",
|
||||
!to && !onClick && alignmentClassName,
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
>
|
||||
{to ? (
|
||||
<Link
|
||||
to={to}
|
||||
className={cn(
|
||||
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-slate-400",
|
||||
alignment === "left"
|
||||
? "justify-start text-left"
|
||||
: alignment === "center"
|
||||
? "justify-center text-center"
|
||||
: "justify-end text-right"
|
||||
)}
|
||||
>
|
||||
<Link to={to} className={flexClasses}>
|
||||
{children}
|
||||
</Link>
|
||||
) : onClick ? (
|
||||
<button onClick={onClick} className={flexClasses}>
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
@@ -171,10 +183,20 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
|
||||
export const TableCellChevron = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
{ className?: string; to: string }
|
||||
>(({ className, to }, ref) => {
|
||||
{
|
||||
className?: string;
|
||||
to?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
>(({ className, to, onClick }, ref) => {
|
||||
return (
|
||||
<TableCell className={className} to={to} ref={ref} alignment="right">
|
||||
<TableCell
|
||||
className={className}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
>
|
||||
<ChevronRightIcon className="h-4 w-4 text-slate-700 transition group-hover:text-bright" />
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {};
|
||||
|
||||
export function TextArea({ className, rows, ...props }: TextAreaProps) {
|
||||
return (
|
||||
<textarea
|
||||
{...props}
|
||||
rows={rows ?? 6}
|
||||
className={cn(
|
||||
"w-full rounded-md border border-slate-800 bg-slate-850 px-3 text-sm text-bright ring-offset-background transition file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground hover:border-slate-750 hover:bg-slate-800 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JobRunStatus } from ".prisma/client";
|
||||
import type { JobRunStatus } from "@trigger.dev/database";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
@@ -83,7 +83,7 @@ export function RunStatusIcon({
|
||||
}
|
||||
}
|
||||
|
||||
type RunBasicStatus =
|
||||
export type RunBasicStatus =
|
||||
| "WAITING"
|
||||
| "PENDING"
|
||||
| "RUNNING"
|
||||
@@ -136,7 +136,7 @@ export function runStatusTitle(status: JobRunStatus): string {
|
||||
}
|
||||
}
|
||||
|
||||
function runStatusClassNameColor(status: JobRunStatus): string {
|
||||
export function runStatusClassNameColor(status: JobRunStatus): string {
|
||||
switch (status) {
|
||||
case "SUCCESS":
|
||||
return "text-green-500";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { StopIcon } from "@heroicons/react/24/outline";
|
||||
import { CheckIcon, ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunList } from "~/presenters/RunListPresenter.server";
|
||||
import { formatDateTime, formatDuration } from "~/utils";
|
||||
import { runPath } from "~/utils/pathBuilder";
|
||||
import { runDashboardPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
@@ -64,7 +64,7 @@ export function RunsTable({
|
||||
</TableBlankRow>
|
||||
) : (
|
||||
runs.map((run) => {
|
||||
const path = runPath(organization, project, job, run);
|
||||
const path = runDashboardPath(organization, project, job, run);
|
||||
return (
|
||||
<TableRow key={run.id}>
|
||||
<TableCell to={path}>#{run.number}</TableCell>
|
||||
|
||||
@@ -85,16 +85,22 @@ function ButtonList({ primary }: { primary: string }) {
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Header3 className="mb-1 uppercase">Shortcut</Header3>
|
||||
<Button variant="primary/small" shortcut="⌘S">
|
||||
<Button
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "s", modifiers: ["meta"] }}
|
||||
>
|
||||
Primary button
|
||||
</Button>
|
||||
<Button variant="secondary/small" shortcut="K">
|
||||
<Button variant="secondary/small" shortcut={{ key: "f" }}>
|
||||
Secondary button
|
||||
</Button>
|
||||
<Button variant="tertiary/small" shortcut="I">
|
||||
<Button variant="tertiary/small" shortcut={{ key: "i" }}>
|
||||
Tertiary button
|
||||
</Button>
|
||||
<Button variant="danger/small" shortcut="⌘S">
|
||||
<Button
|
||||
variant="danger/small"
|
||||
shortcut={{ key: "s", modifiers: ["meta"] }}
|
||||
>
|
||||
Danger button
|
||||
</Button>
|
||||
</div>
|
||||
@@ -195,16 +201,28 @@ function ButtonList({ primary }: { primary: string }) {
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Header3 className="mb-1 uppercase">Shortcut</Header3>
|
||||
<Button variant="primary/medium" shortcut="⌘S">
|
||||
<Button
|
||||
variant="primary/medium"
|
||||
shortcut={{ key: "s", modifiers: ["meta"] }}
|
||||
>
|
||||
Primary button
|
||||
</Button>
|
||||
<Button variant="secondary/medium" shortcut="F">
|
||||
<Button
|
||||
variant="secondary/medium"
|
||||
shortcut={{ key: "s", modifiers: ["meta"] }}
|
||||
>
|
||||
Secondary button
|
||||
</Button>
|
||||
<Button variant="tertiary/medium" shortcut="⌘S">
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
shortcut={{ key: "s", modifiers: ["meta"] }}
|
||||
>
|
||||
Tertiary button
|
||||
</Button>
|
||||
<Button variant="danger/medium" shortcut="⌘S">
|
||||
<Button
|
||||
variant="danger/medium"
|
||||
shortcut={{ key: "s", modifiers: ["meta"] }}
|
||||
>
|
||||
Danger button
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ function BasicCheckBox() {
|
||||
id="check2"
|
||||
variant="button"
|
||||
label="This is a button checkbox with a badge"
|
||||
badge="This is a badge"
|
||||
badges={["This is a badge"]}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<Checkbox
|
||||
@@ -71,7 +71,7 @@ function BasicCheckBox() {
|
||||
name="Description checkbox"
|
||||
id="check3"
|
||||
variant="description"
|
||||
badge="This is a badge"
|
||||
badges={["This is a badge"]}
|
||||
label="This is a checkbox with a description and badge"
|
||||
description="This is a long checkbox description that goes full width. Grants full access to public and private repositories including read and write access to code, commit statuses, repository invitations, collaborators, deployment statuses, and repository webhooks. Note: In addition to repository related resources, the repo scope also grants access to manage organization-owned resources including projects, invitations, team memberships and webhooks. This scope also grants the ability to manage projects owned by users."
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/ClipboardField",
|
||||
decorators: [withDesign],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ClipboardFieldExample>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => <ClipboardFieldExample />,
|
||||
};
|
||||
|
||||
function ClipboardFieldExample() {
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col items-start gap-y-8 p-8">
|
||||
<ClipboardField value="copy paste me" variant="primary/small" />
|
||||
<ClipboardField value="copy paste me" variant="secondary/small" />
|
||||
<ClipboardField value="copy paste me" variant="tertiary/small" />
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/small"
|
||||
icon={<EnvironmentLabel environment={{ type: "PRODUCTION" }} />}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/small"
|
||||
icon="search"
|
||||
/>
|
||||
<ClipboardField value="copy paste me" variant="primary/medium" />
|
||||
<ClipboardField value="copy paste me" variant="secondary/medium" />
|
||||
<ClipboardField value="copy paste me" variant="tertiary/medium" />
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/medium"
|
||||
icon={<EnvironmentLabel environment={{ type: "DEVELOPMENT" }} />}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/medium"
|
||||
icon="search"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-y-8 p-8">
|
||||
<ClipboardField
|
||||
value="copy paste me"
|
||||
variant="primary/small"
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="copy paste me"
|
||||
variant="secondary/small"
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="copy paste me"
|
||||
variant="tertiary/small"
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/small"
|
||||
icon={<EnvironmentLabel environment={{ type: "STAGING" }} />}
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/small"
|
||||
icon="search"
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="copy paste me"
|
||||
variant="primary/medium"
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="copy paste me"
|
||||
variant="secondary/medium"
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="copy paste me"
|
||||
variant="tertiary/medium"
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/medium"
|
||||
icon={<EnvironmentLabel environment={{ type: "PRODUCTION" }} />}
|
||||
secure={true}
|
||||
/>
|
||||
<ClipboardField
|
||||
value="with leadingIcon"
|
||||
variant="tertiary/medium"
|
||||
icon="search"
|
||||
secure={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -128,7 +128,7 @@ function LoginForm() {
|
||||
Continue with Email
|
||||
</Button>
|
||||
<Paragraph variant="small" className="mt-2 text-center">
|
||||
By connecting your GitHub account you agree to our{" "}
|
||||
By creating an account you agree to our{" "}
|
||||
<TextLink href="#">terms</TextLink> and{" "}
|
||||
<TextLink href="#">privacy</TextLink> policies.
|
||||
</Paragraph>
|
||||
|
||||
@@ -42,7 +42,11 @@ function PageHeaders() {
|
||||
<PageTitleRow>
|
||||
<PageTitle title="Organizations" />
|
||||
<PageButtons>
|
||||
<LinkButton to={""} variant="primary/small" shortcut="N">
|
||||
<LinkButton
|
||||
to={""}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
>
|
||||
Create a new Organization
|
||||
</LinkButton>
|
||||
</PageButtons>
|
||||
@@ -61,7 +65,11 @@ function PageHeaders() {
|
||||
backButton={{ to: "#", text: "Orgs" }}
|
||||
/>
|
||||
<PageButtons>
|
||||
<LinkButton to={""} variant="primary/small" shortcut="N">
|
||||
<LinkButton
|
||||
to={""}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
>
|
||||
Create a new Organization
|
||||
</LinkButton>
|
||||
</PageButtons>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import { useState } from "react";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/RadioGroup",
|
||||
decorators: [withDesign],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RadioGroupExample>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => <RadioGroupExample />,
|
||||
};
|
||||
|
||||
function RadioGroupExample() {
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-8 p-8">
|
||||
<Button
|
||||
onClick={() => setIsDisabled((d) => !d)}
|
||||
variant="primary/medium"
|
||||
className="max-w-fit"
|
||||
>
|
||||
{isDisabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
<form>
|
||||
<RadioGroup name="simple" disabled={isDisabled} className="grid gap-2">
|
||||
<RadioGroupItem
|
||||
id="r2"
|
||||
label="Simple small"
|
||||
value={"1"}
|
||||
variant="simple/small"
|
||||
/>
|
||||
<RadioGroupItem id="r3" label="Simple" value={"2"} variant="simple" />
|
||||
<RadioGroupItem
|
||||
id="r4"
|
||||
label="Button small"
|
||||
value={"3"}
|
||||
variant="button/small"
|
||||
/>
|
||||
<RadioGroupItem id="r5" label="Button" value={"4"} variant="button" />
|
||||
<RadioGroupItem
|
||||
id="r6"
|
||||
label="This is a label"
|
||||
description="This is a description"
|
||||
value={"5"}
|
||||
variant="description"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Switch } from "../primitives/Switch";
|
||||
import { ShortcutKey } from "../primitives/ShortcutKey";
|
||||
import { ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { OperatingSystemContextProvider } from "../primitives/OperatingSystemProvider";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/ShortcutKey",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Collection>;
|
||||
|
||||
export const ShortcutKeys: Story = {
|
||||
render: () => <Collection />,
|
||||
};
|
||||
|
||||
const shortcuts: ShortcutDefinition[] = [
|
||||
{ key: "esc" },
|
||||
{ key: "f" },
|
||||
{ key: "f", modifiers: ["meta"] },
|
||||
{ key: "k", modifiers: ["meta"] },
|
||||
{ key: "del", modifiers: ["ctrl", "alt"] },
|
||||
];
|
||||
|
||||
function Collection() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-4">
|
||||
<Set platform="mac" />
|
||||
<Set platform="windows" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Set({ platform }: { platform: "mac" | "windows" }) {
|
||||
return (
|
||||
<OperatingSystemContextProvider platform={platform}>
|
||||
<Header1>{platform}</Header1>
|
||||
{shortcuts.map((shortcut, index) => (
|
||||
<div key={index} className="flex items-center gap-x-2">
|
||||
<ShortcutKey shortcut={shortcut} variant="small" />
|
||||
<ShortcutKey shortcut={shortcut} variant="medium" />
|
||||
<Button variant="primary/small" shortcut={shortcut}>
|
||||
Primary small
|
||||
</Button>
|
||||
<Button variant="secondary/small" shortcut={shortcut}>
|
||||
Secondary small
|
||||
</Button>
|
||||
<Button variant="tertiary/small" shortcut={shortcut}>
|
||||
Tertiary small
|
||||
</Button>
|
||||
<Button variant="danger/small" shortcut={shortcut}>
|
||||
Danger small
|
||||
</Button>
|
||||
<Button variant="primary/medium" shortcut={shortcut}>
|
||||
Primary medium
|
||||
</Button>
|
||||
<Button variant="secondary/medium" shortcut={shortcut}>
|
||||
Secondary medium
|
||||
</Button>
|
||||
<Button variant="tertiary/medium" shortcut={shortcut}>
|
||||
Tertiary medium
|
||||
</Button>
|
||||
<Button variant="danger/medium" shortcut={shortcut}>
|
||||
Danger medium
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</OperatingSystemContextProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Switch } from "../primitives/Switch";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/Switch",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Collection>;
|
||||
|
||||
export const Switches: Story = {
|
||||
render: () => <Collection />,
|
||||
};
|
||||
|
||||
function Collection() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-4 p-4">
|
||||
<Switch variant="large" />
|
||||
<Switch variant="large" disabled />
|
||||
<Switch variant="large" label="Toggle me" />
|
||||
<Switch variant="large" label="Toggle me" disabled />
|
||||
<Switch variant="small" />
|
||||
<Switch variant="small" disabled />
|
||||
<Switch variant="small" label="Toggle me" />
|
||||
<Switch variant="small" label="Toggle me" disabled />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { PrismaClient, Prisma } from ".prisma/client";
|
||||
import { PrismaClient, Prisma } from "@trigger.dev/database";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
|
||||
export type PrismaTransactionClient = Omit<
|
||||
PrismaClient,
|
||||
"$connect" | "$disconnect" | "$on" | "$transaction" | "$use"
|
||||
"$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"
|
||||
>;
|
||||
|
||||
export type PrismaClientOrTransaction = PrismaClient | PrismaTransactionClient;
|
||||
@@ -96,7 +96,7 @@ function getClient() {
|
||||
}
|
||||
|
||||
export { prisma };
|
||||
export type { PrismaClient } from ".prisma/client";
|
||||
export type { PrismaClient } from "@trigger.dev/database";
|
||||
|
||||
export const PrismaErrorSchema = z.object({
|
||||
code: z.string(),
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
import { RemixBrowser, useLocation, useMatches } from "@remix-run/react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import * as Sentry from "@sentry/remix";
|
||||
import { useEffect } from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
|
||||
import { OperatingSystemContextProvider } from "./components/primitives/OperatingSystemProvider";
|
||||
|
||||
hydrateRoot(
|
||||
document,
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixBrowser />
|
||||
</LocaleContextProvider>
|
||||
<OperatingSystemContextProvider
|
||||
platform={window.navigator.userAgent.includes("Mac") ? "mac" : "windows"}
|
||||
>
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixBrowser />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Sentry.init({
|
||||
dsn: "https://bf96820b08004fa4b2e1506f2ac74a14@o4504419574087680.ingest.sentry.io/4504419607052288",
|
||||
tracesSampleRate: 1,
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.remixRouterInstrumentation(
|
||||
useEffect,
|
||||
useLocation,
|
||||
useMatches
|
||||
),
|
||||
}),
|
||||
//casted because TypeScript is unhappy about the type from PostHog
|
||||
new posthog.SentryIntegration(
|
||||
posthog,
|
||||
"triggerdev",
|
||||
4504419607052288
|
||||
) as any,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Sentry from "~/services/sentry.server";
|
||||
import * as Worker from "~/services/worker.server";
|
||||
import { PassThrough } from "stream";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
@@ -8,6 +7,10 @@ import type { EntryContext, Headers } from "@remix-run/node"; // or cloudflare/d
|
||||
import { parseAcceptLanguage } from "intl-parse-accept-language";
|
||||
import isbot from "isbot";
|
||||
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
|
||||
import {
|
||||
OperatingSystemContextProvider,
|
||||
OperatingSystemPlatform,
|
||||
} from "./components/primitives/OperatingSystemProvider";
|
||||
|
||||
const ABORT_DELAY = 30000;
|
||||
|
||||
@@ -22,6 +25,13 @@ export default function handleRequest(
|
||||
validate: Intl.DateTimeFormat.supportedLocalesOf,
|
||||
});
|
||||
|
||||
//get whether it's a mac or pc from the headers
|
||||
const platform: OperatingSystemPlatform = request.headers
|
||||
.get("user-agent")
|
||||
?.includes("Mac")
|
||||
? "mac"
|
||||
: "windows";
|
||||
|
||||
// If the request is from a bot, we want to wait for the full
|
||||
// response to render before sending it to the client. This
|
||||
// ensures that bots can see the full page content.
|
||||
@@ -31,7 +41,8 @@ export default function handleRequest(
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext,
|
||||
locales
|
||||
locales,
|
||||
platform
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +51,8 @@ export default function handleRequest(
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext,
|
||||
locales
|
||||
locales,
|
||||
platform
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,17 +61,20 @@ function serveTheBots(
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
locales: string[]
|
||||
locales: string[],
|
||||
platform: OperatingSystemPlatform
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>
|
||||
</LocaleContextProvider>,
|
||||
<OperatingSystemContextProvider platform={platform}>
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>,
|
||||
{
|
||||
// Use onAllReady to wait for the entire document to be ready
|
||||
onAllReady() {
|
||||
@@ -87,18 +102,21 @@ function serveBrowsers(
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
locales: string[]
|
||||
locales: string[],
|
||||
platform: OperatingSystemPlatform
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>
|
||||
</LocaleContextProvider>,
|
||||
<OperatingSystemContextProvider platform={platform}>
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>,
|
||||
{
|
||||
// use onShellReady to wait until a suspense boundary is triggered
|
||||
onShellReady() {
|
||||
@@ -125,5 +143,4 @@ function serveBrowsers(
|
||||
});
|
||||
}
|
||||
|
||||
Sentry.init();
|
||||
Worker.init().catch(console.error);
|
||||
|
||||
@@ -17,7 +17,6 @@ const EnvironmentSchema = z.object({
|
||||
.string()
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
.default(process.env.FC_URL ?? "https://app.trigger.dev"),
|
||||
SENTRY_DSN: z.string().optional(),
|
||||
APP_ENV: z
|
||||
.union([
|
||||
z.literal("development"),
|
||||
@@ -29,12 +28,13 @@ const EnvironmentSchema = z.object({
|
||||
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
|
||||
POSTHOG_PROJECT_KEY: z.string().optional(),
|
||||
MAGIC_LINK_SECRET: z.string(),
|
||||
GITHUB_CLIENT_ID: z.string(),
|
||||
GITHUB_SECRET: z.string(),
|
||||
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
FROM_EMAIL: z.string(),
|
||||
REPLY_TO_EMAIL: z.string(),
|
||||
RESEND_API_KEY: z.string(),
|
||||
SESSION_SECRET: z.string(),
|
||||
PLAIN_API_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { requestUrl } from "./utils";
|
||||
|
||||
export type TriggerFeatures = {
|
||||
isManagedCloud: boolean;
|
||||
};
|
||||
|
||||
// If the request host is cloud.trigger.dev then we are on the managed cloud
|
||||
// or if env.NODE_ENV is development
|
||||
export function featuresForRequest(request: Request): TriggerFeatures {
|
||||
const url = requestUrl(request);
|
||||
|
||||
const isManagedCloud =
|
||||
url.host === "cloud.trigger.dev" || process.env.NODE_ENV === "development";
|
||||
|
||||
return {
|
||||
isManagedCloud,
|
||||
};
|
||||
}
|
||||
@@ -1,53 +1,30 @@
|
||||
import { DEV_ENVIRONMENT, LIVE_ENVIRONMENT } from "~/consts";
|
||||
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { useMatchesData } from "~/utils";
|
||||
import { MatchedProject, useOptionalProject } from "./useProject";
|
||||
|
||||
function isRuntimeEnvironment(
|
||||
environment: any
|
||||
): environment is RuntimeEnvironment {
|
||||
return (
|
||||
environment &&
|
||||
typeof environment === "object" &&
|
||||
typeof environment.slug === "string"
|
||||
);
|
||||
}
|
||||
export type ProjectJobEnvironment = MatchedProject["environments"][number];
|
||||
|
||||
function isRuntimeEnvironments(
|
||||
environments: any
|
||||
): environments is RuntimeEnvironment[] {
|
||||
return (
|
||||
environments &&
|
||||
typeof environments === "object" &&
|
||||
Array.isArray(environments) &&
|
||||
environments.every(isRuntimeEnvironment)
|
||||
);
|
||||
}
|
||||
export function useEnvironments() {
|
||||
const project = useOptionalProject();
|
||||
|
||||
export function useEnvironments(): RuntimeEnvironment[] | undefined {
|
||||
const routeMatch = useMatchesData("routes/_app.orgs.$organizationSlug");
|
||||
|
||||
if (
|
||||
!routeMatch ||
|
||||
!isRuntimeEnvironments(routeMatch.data.organization.environments)
|
||||
) {
|
||||
if (!project) {
|
||||
return undefined;
|
||||
}
|
||||
return routeMatch.data.organization.environments;
|
||||
|
||||
return project.environments;
|
||||
}
|
||||
|
||||
export function useDevEnvironment(): RuntimeEnvironment | undefined {
|
||||
export function useDevEnvironment() {
|
||||
const environments = useEnvironments();
|
||||
if (!environments) return;
|
||||
|
||||
return environments.find(
|
||||
(environment: any) => environment.slug === DEV_ENVIRONMENT
|
||||
);
|
||||
return environments.find((environment) => environment.type === "DEVELOPMENT");
|
||||
}
|
||||
|
||||
export function useLiveEnvironment(): RuntimeEnvironment | undefined {
|
||||
export function useProdEnvironment() {
|
||||
const environments = useEnvironments();
|
||||
if (!environments) return;
|
||||
return environments.find(
|
||||
(environment: any) => environment.slug === LIVE_ENVIRONMENT
|
||||
);
|
||||
|
||||
return environments.find((environment) => environment.type === "PRODUCTION");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useTypedRouteLoaderData } from "remix-typedjson";
|
||||
import { loader } from "../root";
|
||||
import type { TriggerFeatures } from "~/features.server";
|
||||
|
||||
export function useFeatures(): TriggerFeatures {
|
||||
const routeMatch = useTypedRouteLoaderData<typeof loader>("root");
|
||||
|
||||
return routeMatch?.features ?? { isManagedCloud: false };
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ProjectJob } from "./useProject";
|
||||
import { useTextFilter } from "./useTextFilter";
|
||||
|
||||
export function useFilterJobs(jobs: ProjectJob[]) {
|
||||
const { filterText, setFilterText, filteredItems } =
|
||||
useTextFilter<ProjectJob>({
|
||||
items: jobs,
|
||||
filter: (job, text) => {
|
||||
if (job.slug.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (job.title.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (job.event.title.toLowerCase().includes(text.toLowerCase()))
|
||||
return true;
|
||||
if (
|
||||
job.integrations.some((integration) =>
|
||||
integration.title.toLowerCase().includes(text.toLowerCase())
|
||||
)
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
job.properties &&
|
||||
job.properties.some((property) =>
|
||||
property.text.toLowerCase().includes(text.toLowerCase())
|
||||
)
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return { filterText, setFilterText, filteredItems };
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
UseDataFunctionReturn,
|
||||
useTypedRouteLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route";
|
||||
|
||||
export type MatchedClient = UseDataFunctionReturn<typeof loader>["client"];
|
||||
|
||||
export function useOptionalIntegrationClient() {
|
||||
const routeMatch = useTypedRouteLoaderData<typeof loader>(
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam"
|
||||
);
|
||||
|
||||
return routeMatch?.client;
|
||||
}
|
||||
|
||||
export function useIntegrationClient() {
|
||||
const client = useOptionalIntegrationClient();
|
||||
invariant(client, "Client must be defined");
|
||||
return client;
|
||||
}
|
||||
@@ -1,34 +1,29 @@
|
||||
import type { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import type { loader as jobLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
|
||||
import { hydrateObject, useMatchesData } from "~/utils";
|
||||
import { useOptionalProject } from "./useProject";
|
||||
import {
|
||||
UseDataFunctionReturn,
|
||||
useTypedRouteLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
|
||||
import { useOptionalProject } from "./useProject";
|
||||
|
||||
export type MatchedJob = UseDataFunctionReturn<typeof jobLoader>["job"];
|
||||
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
|
||||
|
||||
export function useOptionalJob() {
|
||||
const project = useOptionalProject();
|
||||
const routeMatch = useMatchesData(
|
||||
const routeMatch = useTypedRouteLoaderData<typeof loader>(
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam"
|
||||
);
|
||||
|
||||
if (!project || !routeMatch || !routeMatch.data.job) {
|
||||
if (!project || !routeMatch || !routeMatch.job) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (routeMatch.data.job == null) {
|
||||
if (routeMatch.job == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = hydrateObject<UseDataFunctionReturn<typeof jobLoader>["job"]>(
|
||||
routeMatch.data.job
|
||||
);
|
||||
|
||||
if (result == null) return undefined;
|
||||
|
||||
//get the job from the list on the project
|
||||
const job = project.jobs.find((j) => j.id === result.id);
|
||||
return job;
|
||||
return project.jobs.find((j) => j.id === routeMatch.job.id);
|
||||
}
|
||||
|
||||
export function useJob() {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import type { loader as appLoader } from "~/routes/_app/route";
|
||||
import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { hydrateObject, useMatchesData } from "~/utils";
|
||||
import {
|
||||
UseDataFunctionReturn,
|
||||
useTypedRouteLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import type { loader as appLoader } from "~/routes/_app/route";
|
||||
import { hydrateObject, useMatchesData } from "~/utils";
|
||||
|
||||
export type MatchedOrganization = UseDataFunctionReturn<
|
||||
typeof appLoader
|
||||
@@ -20,24 +23,19 @@ export function useOrganizations() {
|
||||
|
||||
export function useOptionalOrganization() {
|
||||
const orgs = useOptionalOrganizations();
|
||||
const routeMatch = useMatchesData("routes/_app.orgs.$organizationSlug");
|
||||
const routeMatch = useTypedRouteLoaderData<typeof orgLoader>(
|
||||
"routes/_app.orgs.$organizationSlug"
|
||||
);
|
||||
|
||||
if (!orgs || !routeMatch || !routeMatch.data.organization) {
|
||||
if (!orgs || !routeMatch || !routeMatch.organization) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (routeMatch.data.organization == null) {
|
||||
if (routeMatch.organization === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = hydrateObject<
|
||||
UseDataFunctionReturn<typeof orgLoader>["organization"]
|
||||
>(routeMatch.data.organization);
|
||||
|
||||
if (result == null) return undefined;
|
||||
|
||||
const org = orgs.find((o) => o.id === result.id);
|
||||
return org;
|
||||
return orgs.find((o) => o.id === routeMatch.organization.id);
|
||||
}
|
||||
|
||||
export function useOrganization() {
|
||||
|
||||
@@ -14,6 +14,7 @@ export const usePostHog = (apiKey?: string, logging = false): void => {
|
||||
//start PostHog once
|
||||
useEffect(() => {
|
||||
if (apiKey === undefined) return;
|
||||
if (apiKey === "") return;
|
||||
if (postHogInitialized.current === true) return;
|
||||
if (logging) console.log("posthog.init", apiKey);
|
||||
postHogInitialized.current = true;
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import type { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import {
|
||||
UseDataFunctionReturn,
|
||||
useTypedRouteLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader as projectLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
|
||||
import { hydrateObject, useMatchesData } from "~/utils";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
|
||||
|
||||
export type MatchedProject = UseDataFunctionReturn<
|
||||
typeof projectLoader
|
||||
>["project"];
|
||||
export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
|
||||
|
||||
export type ProjectJob = MatchedProject["jobs"][number];
|
||||
|
||||
export function useOptionalProject() {
|
||||
const routeMatch = useMatchesData(
|
||||
const routeMatch = useTypedRouteLoaderData<typeof loader>(
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam"
|
||||
);
|
||||
|
||||
if (!routeMatch || !routeMatch.data.project) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = hydrateObject<
|
||||
UseDataFunctionReturn<typeof projectLoader>["project"]
|
||||
>(routeMatch.data.project);
|
||||
|
||||
if (result == null) return undefined;
|
||||
return result;
|
||||
return routeMatch?.project;
|
||||
}
|
||||
|
||||
export function useProject() {
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import type { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route";
|
||||
import { hydrateObject, useMatchesData } from "~/utils";
|
||||
import { useOptionalProject } from "./useProject";
|
||||
import {
|
||||
UseDataFunctionReturn,
|
||||
useTypedRouteLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route";
|
||||
import { useOptionalProject } from "./useProject";
|
||||
|
||||
export type MatchedRun = UseDataFunctionReturn<typeof runLoader>["run"];
|
||||
|
||||
export function useOptionalRun() {
|
||||
const project = useOptionalProject();
|
||||
const routeMatch = useMatchesData(
|
||||
const routeMatch = useTypedRouteLoaderData<typeof runLoader>(
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam"
|
||||
);
|
||||
|
||||
if (!project || !routeMatch || !routeMatch.data.run) {
|
||||
if (!project || !routeMatch || !routeMatch.run) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (routeMatch.data.run == null) {
|
||||
if (routeMatch.run == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const run = hydrateObject<UseDataFunctionReturn<typeof runLoader>["run"]>(
|
||||
routeMatch.data.run
|
||||
);
|
||||
|
||||
if (run == null) return undefined;
|
||||
|
||||
return run;
|
||||
return routeMatch.run;
|
||||
}
|
||||
|
||||
export function useRun() {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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 Shortcut = {
|
||||
key: string;
|
||||
modifiers?: Modifier[];
|
||||
};
|
||||
|
||||
export type ShortcutDefinition =
|
||||
| {
|
||||
windows: Shortcut;
|
||||
mac: Shortcut;
|
||||
}
|
||||
| Shortcut;
|
||||
|
||||
type useShortcutKeysProps = {
|
||||
shortcut: ShortcutDefinition;
|
||||
action: (event: KeyboardEvent) => void;
|
||||
disabled?: 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;
|
||||
|
||||
return modifiers ? modifiers.map((k) => k).join("+") + "+" : "" + character;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export type {
|
||||
ApiConnectionType,
|
||||
ApiConnectionClient,
|
||||
ApiConnection,
|
||||
} from ".prisma/client";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user