Compare commits
415 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b35cc35e0 | |||
| fade22015f | |||
| 78af477efc | |||
| e864973e01 | |||
| 132008ff64 | |||
| 26d4d08bdc | |||
| 1a8880971d | |||
| 3d17ce5559 | |||
| a76905e272 | |||
| 1c73ab930b | |||
| 816c7ad56e | |||
| 4d975b19e7 | |||
| 4c4b774bab | |||
| fa7f4b1fed | |||
| f76765448b | |||
| 944b1875f1 | |||
| a5dd6389b2 | |||
| 3ec95cd72d | |||
| 6f29b7dc34 | |||
| ed07bf9306 | |||
| cd6a0b3a3e | |||
| e9cc7de8ea | |||
| 0448f3603a | |||
| dd4515a2eb | |||
| 97e5c22051 | |||
| b8d2fb215f | |||
| 21c2c136be | |||
| ae19d0f491 | |||
| c58b78f43f | |||
| a968c23678 | |||
| 4b08597c77 | |||
| 9ce4f57a6a | |||
| 363b9b15b4 | |||
| 51305b6023 | |||
| 9e41e83818 | |||
| da7dc74fa4 | |||
| af14621683 | |||
| 9787120cfb | |||
| e46e9bd30f | |||
| 3cd7dd86d4 | |||
| a782813c6c | |||
| d950a969bd | |||
| 8eecc7e548 | |||
| 1294076484 | |||
| f82a4e9651 | |||
| 1f4434035e | |||
| 7bb88451be | |||
| be70f42bec | |||
| bb54e97806 | |||
| edafc71142 | |||
| 792c97339e | |||
| 0fc234b970 | |||
| f8126871f3 | |||
| df0dc07678 | |||
| 63857b0445 | |||
| b985843642 | |||
| 9ecda816dc | |||
| 7b54c3527e | |||
| 60b8c76adc | |||
| 3fd8ea80da | |||
| c576e98dbc | |||
| b9e62139ff | |||
| 44af80e055 | |||
| 1feabd732e | |||
| 134942b2dd | |||
| b153324cd9 | |||
| 59bfce86db | |||
| 748565c9db | |||
| 8b31871998 | |||
| 14dcc76f93 | |||
| 5ea6605ec6 | |||
| 3e1cc69db9 | |||
| 2ea52da628 | |||
| 3493b9464f | |||
| a7ff6de388 | |||
| 0d136a3e0c | |||
| c8165ccd44 | |||
| 00cc07dad3 | |||
| 71693eb9b3 | |||
| fb59bfd8f8 | |||
| 038ee75c28 | |||
| 09d0e80804 | |||
| 74808d7400 | |||
| 6791328a94 | |||
| 3dce1f66f7 | |||
| 17bb0c0cef | |||
| ef59a62368 | |||
| b447a8040f | |||
| ed86b4f5db | |||
| 9b0eb64035 | |||
| c927fbc8e4 | |||
| bf4fff9cca | |||
| ad1672d95f | |||
| 6df7eb2bb5 | |||
| 36ced50c04 | |||
| 684e02c8c8 | |||
| 4b9f33082e | |||
| a90b73c7ca | |||
| ff018718f7 | |||
| 07980f8ebd | |||
| 8d5c86fea0 | |||
| a9b0b4f7c9 | |||
| 4dee2c360e | |||
| 4565f19d3a | |||
| 0796df2aa9 | |||
| 6bf580ac21 | |||
| 3ad4b8b32d | |||
| f685c6637d | |||
| 1dbcf661f3 | |||
| 6f6852621d | |||
| aa3a56a85d | |||
| 5e999c8992 | |||
| 569cecb675 | |||
| c0b6c41afc | |||
| 35c7a1ecb0 | |||
| a64875a297 | |||
| 8690878a86 | |||
| b78b3dceb2 | |||
| 630e9556b0 | |||
| 6db1b71202 | |||
| 70b4b1257e | |||
| 87c21ab50a | |||
| 4c635f72a2 | |||
| 46dad7dc76 | |||
| 4fdf23b38b | |||
| 0c71dc74df | |||
| 6ff0b923e1 | |||
| 1a64013825 | |||
| f456f68f88 | |||
| 28b6be2491 | |||
| dbcf632b4e | |||
| 3cfde48bfd | |||
| ba2e0cc1c6 | |||
| b113e5a99c | |||
| b119a52e08 | |||
| e02c90a4ea | |||
| 493ca11c43 | |||
| b36d81a622 | |||
| 275ce9f290 | |||
| 5cff5c45e9 | |||
| 47e583c19d | |||
| 1019c9cdea | |||
| 469a8df0cf | |||
| 922c7cb3a6 | |||
| 0fcb9705ae | |||
| 74fdfa93c5 | |||
| bdaa2ed15a | |||
| afc9a6ade1 | |||
| 7bb7e7aedc | |||
| e90235b317 | |||
| b7134dd769 | |||
| 6fea802505 | |||
| 09ef7625ff | |||
| 5cf37e9f8a | |||
| e7b6c2fab6 | |||
| d619e79f74 | |||
| 38a94b3b70 | |||
| 0c2af6d87c | |||
| 0deb2c640d | |||
| 21c70ad673 | |||
| 0793c2b0f4 | |||
| 8dc90d61b4 | |||
| 57f3a89a86 | |||
| ceb737e9f1 | |||
| 632d6b91e9 | |||
| b548561220 | |||
| aecee4d35f | |||
| 9ab590bead | |||
| 2cc1453f25 | |||
| 82f3db6825 | |||
| f0bc4ad2bd | |||
| 4d15c8f7cf | |||
| 7f3b1c1735 | |||
| 707bc09dac | |||
| a0f2c982f9 | |||
| bffd18a6b8 | |||
| 2018b41d22 | |||
| c41d63497b | |||
| 56146afcb6 | |||
| 020565c009 | |||
| 58d56f8381 | |||
| 5b49b420cb | |||
| 3fb68b784a | |||
| a414e65efe | |||
| 38fe854559 | |||
| c24f3a56cc | |||
| 8798352be3 | |||
| 1a1e70ae3d | |||
| 19bdf3b6ea | |||
| 427c450391 | |||
| 9374aea401 | |||
| fd3db5f77b | |||
| e2049fcfbf | |||
| 1be4fcbb82 | |||
| 124435e5e7 | |||
| 4a79b3b35b | |||
| 0da6a19987 | |||
| 08d84eb596 | |||
| f38d35e9f6 | |||
| 1b4dabb8ba | |||
| aa24a9aae6 | |||
| dc568e7f3e | |||
| fa6675eb00 | |||
| 8a86e7b41d | |||
| 119bfc0068 | |||
| 7a6d9dc225 | |||
| 0f3179f493 | |||
| b87e1c4630 | |||
| 14058d557f | |||
| 6535cf918b | |||
| aaf40c3be3 | |||
| 35dedee1eb | |||
| 485f71c8d4 | |||
| ec2175fec8 | |||
| fb9abe5289 | |||
| f2db1b8aa5 | |||
| 3c93783e8b | |||
| 55f41a2168 | |||
| 4ca6506655 | |||
| 9f9e74b0b6 | |||
| ea6f66bdfc | |||
| a42c9acf44 | |||
| f7f8bc28c5 | |||
| 5f6fd8e235 | |||
| 33e1008d83 | |||
| a0815c8e3e | |||
| 8a5e7f34b1 | |||
| c3dca5835d | |||
| 20a8e7c6c1 | |||
| cc5514e57b | |||
| ab432a2d7d | |||
| e12c82b444 | |||
| 808c299008 | |||
| 498b9a21af | |||
| 48e1e7250b | |||
| 3f2b748a80 | |||
| a060ceef0b | |||
| e6fb32194e | |||
| b38405cb88 | |||
| e617c14456 | |||
| 15fd731f3d | |||
| 4c7742c258 | |||
| e72820c080 | |||
| 7a34c1102b | |||
| 2b3d54aff5 | |||
| bb1f0a711a | |||
| 47b19a49d4 | |||
| 5301239e09 | |||
| 261dce102a | |||
| 575413c414 | |||
| e7795a06ad | |||
| 2b3ea692fe | |||
| f603725393 | |||
| a61951049f | |||
| 7ee0992a2f | |||
| eee0fb2198 | |||
| 1b62b348a9 | |||
| a2a5204ac5 | |||
| d4a9cefd78 | |||
| 9fca9e0508 | |||
| d6dd2c9210 | |||
| 01e7ef3d04 | |||
| e84d8cfa1d | |||
| 26b0523cd0 | |||
| 84d8fb575f | |||
| 80d449bfdb | |||
| ff157e51a1 | |||
| a027e6a1bb | |||
| 5fdc7e66af | |||
| ea853b243e | |||
| 12f4653cd6 | |||
| 47f4726515 | |||
| 7ecaf2ce53 | |||
| 7b463710f5 | |||
| fda9565eb0 | |||
| 82e7484da8 | |||
| 0f66023b54 | |||
| 021d6d8f20 | |||
| 068c024477 | |||
| b74c000fb6 | |||
| 0410316e14 | |||
| c571c5ef32 | |||
| e84ede9599 | |||
| 3fc5630be7 | |||
| 13d378f6df | |||
| 1a6a9ca97a | |||
| 6cb57b803c | |||
| 7c791dd519 | |||
| cf3735976e | |||
| a9a40b5184 | |||
| 0f928e224b | |||
| a306545952 | |||
| 1bb1fc4c8f | |||
| 1ca37f5745 | |||
| 8112c12092 | |||
| c8d252e300 | |||
| d89f740d18 | |||
| 41a3bdec68 | |||
| a9a937dd25 | |||
| 14e081e17d | |||
| 500a173d67 | |||
| cc562c58fa | |||
| 0334df70ad | |||
| e08e493b3c | |||
| f667d61d4c | |||
| 325ef04c7f | |||
| 79f0a6a662 | |||
| ad5027b5fd | |||
| f0809e6ad6 | |||
| c63559f55d | |||
| 8471169b30 | |||
| 558a39c644 | |||
| b4ab06600b | |||
| c8e8bc9306 | |||
| a69621bdcc | |||
| ad4daa3301 | |||
| bb7173b4ab | |||
| 2aee3debc0 | |||
| d1e40643fc | |||
| a8e38d9231 | |||
| 2ae0e22d81 | |||
| 206a2c3245 | |||
| eb3929880f | |||
| 0661ee5fdd | |||
| 2deef00d48 | |||
| fa5360a99d | |||
| 7ace1c3e25 | |||
| a815633712 | |||
| 84a0336bdd | |||
| 7be07b4d0e | |||
| bb96e848de | |||
| 39f03085b4 | |||
| 21c0dc619e | |||
| da0ef73724 | |||
| 65da20c225 | |||
| ccc9764383 | |||
| 584722d48b | |||
| 1b1ad16d94 | |||
| 37aadfc80b | |||
| 6736cb5ae9 | |||
| 87630bc64f | |||
| d23fa38a0a | |||
| 4ec0395424 | |||
| e94558a0d5 | |||
| 6f4af89adb | |||
| 3e45f785a0 | |||
| 04b9973f20 | |||
| 8da3043111 | |||
| 2b586c87a0 | |||
| 053389d9b3 | |||
| 43a4139424 | |||
| 7b509849e9 | |||
| 13f750a18b | |||
| 15816e9955 | |||
| a10e2a8b16 | |||
| 423209661b | |||
| 73f363f9d5 | |||
| 4456184f37 | |||
| 2448242be3 | |||
| 9c3aeb3efb | |||
| df0bce3a8f | |||
| 0fe85eaec4 | |||
| d2dde0a844 | |||
| aacec569d0 | |||
| 0df7af4403 | |||
| 553a03580f | |||
| af019473cd | |||
| cb47200d34 | |||
| 98fd8045e9 | |||
| 44184a61c7 | |||
| c0807ad0d0 | |||
| cf9460afee | |||
| 7fe111abe9 | |||
| 525285da7f | |||
| 07365cd405 | |||
| 83d0e8710d | |||
| 280a6d0297 | |||
| f579afb4cc | |||
| 3d744c6ecc | |||
| 5691480dac | |||
| 09a859d056 | |||
| 7bdbbdc433 | |||
| 8b0e4f2367 | |||
| e837500486 | |||
| 60fcf5fdac | |||
| 3a62ee36d9 | |||
| bb606a1fcd | |||
| e9c6cc71f0 | |||
| 30f93521c8 | |||
| 18f77660e0 | |||
| aa1027969b | |||
| 32ff569844 | |||
| dfca04aaea | |||
| 672a6b8be7 | |||
| 56ec0523b5 | |||
| 73214d760e | |||
| 853a7ef7e4 | |||
| 0726620525 | |||
| cfb77159d7 | |||
| 2dbc085bb3 | |||
| 26aad58abd | |||
| b62c17bec1 | |||
| fab565256e | |||
| cedd932f99 | |||
| e379fc15be | |||
| c0172d428d | |||
| 2c19693cfc | |||
| 64ca9dc9a8 | |||
| 39e535772f | |||
| 006951aa53 | |||
| 0ebecbe574 | |||
| aa74260646 | |||
| f683fe8d2d | |||
| 863ecf4830 | |||
| bbf397e661 |
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/react-hooks": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/build": patch
|
||||
"@trigger.dev/core": patch
|
||||
"@trigger.dev/rsc": patch
|
||||
---
|
||||
|
||||
Run Engine 2.0 (alpha)
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/redis-worker": major
|
||||
"@trigger.dev/react-hooks": major
|
||||
"@trigger.dev/sdk": major
|
||||
"trigger.dev": major
|
||||
"@trigger.dev/python": major
|
||||
"@trigger.dev/build": major
|
||||
"@trigger.dev/core": major
|
||||
"@trigger.dev/rsc": major
|
||||
---
|
||||
|
||||
Trigger.dev v4 release. Please see our upgrade to v4 docs to view the full changelog: https://trigger.dev/docs/upgrade-to-v4
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
The envvars.list() and retrieve() functions receive isSecret for each value. Secret values are always redacted.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Deprecate toolTask and replace with `ai.tool(mySchemaTask)`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fix init.ts in custom trigger dirs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Init command will now correctly install v4-beta packages
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Improve warm start times by eagerly creating the child TaskRunProcess when a previous run as completed
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Update nypm package to support test-based bun.lock files
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "v4-beta",
|
||||
"initialVersions": {
|
||||
"coordinator": "0.0.1",
|
||||
"docker-provider": "0.0.1",
|
||||
"kubernetes-provider": "0.0.1",
|
||||
"supervisor": "0.0.1",
|
||||
"webapp": "1.0.0",
|
||||
"@trigger.dev/build": "3.3.17",
|
||||
"trigger.dev": "3.3.17",
|
||||
"@trigger.dev/core": "3.3.17",
|
||||
"@trigger.dev/python": "3.3.17",
|
||||
"@trigger.dev/react-hooks": "3.3.17",
|
||||
"@trigger.dev/redis-worker": "3.3.17",
|
||||
"@trigger.dev/rsc": "3.3.17",
|
||||
"@trigger.dev/sdk": "3.3.17"
|
||||
},
|
||||
"changesets": [
|
||||
"breezy-turtles-talk",
|
||||
"four-needles-add",
|
||||
"green-lions-relate",
|
||||
"honest-files-decide",
|
||||
"late-chairs-ring",
|
||||
"moody-squids-count",
|
||||
"nice-colts-boil",
|
||||
"polite-lies-fix",
|
||||
"red-wasps-cover",
|
||||
"shiny-kiwis-beam",
|
||||
"smart-coins-hammer",
|
||||
"sour-mirrors-accept",
|
||||
"tiny-buckets-teach",
|
||||
"tricky-houses-invite",
|
||||
"two-tigers-dream",
|
||||
"weak-jobs-hide"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Suppress external instrumentation for fetch calls from ApiClient
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Handle flush errors gracefully in dev
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
fix: Realtime streams: prevent enqueuing into closed ReadableStream
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Improve usage flushing
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fix stalled run detection
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Managed run controller performance and reliability improvements
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
maintain proper context in metadata.root and parent getters
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
v4: New lifecycle hooks
|
||||
@@ -431,28 +431,6 @@ export async function POST(request: Request) {
|
||||
}
|
||||
```
|
||||
|
||||
### tasks.triggerAndPoll()
|
||||
|
||||
Triggers a task and polls until completion. Not recommended for web requests as it blocks until the run completes. Consider using Realtime docs for better alternatives.
|
||||
|
||||
```ts
|
||||
import { tasks } from "@trigger.dev/sdk/v3";
|
||||
import type { emailSequence } from "~/trigger/emails";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const data = await request.json();
|
||||
const result = await tasks.triggerAndPoll<typeof emailSequence>(
|
||||
"email-sequence",
|
||||
{
|
||||
to: data.email,
|
||||
name: data.name,
|
||||
},
|
||||
{ pollIntervalMs: 5000 }
|
||||
);
|
||||
return Response.json(result);
|
||||
}
|
||||
```
|
||||
|
||||
### batch.trigger()
|
||||
|
||||
Triggers multiple runs of different tasks at once, useful when you need to execute multiple tasks simultaneously.
|
||||
|
||||
+3
-1
@@ -4,4 +4,6 @@ apps/proxy/
|
||||
apps/coordinator/
|
||||
packages/rsc/
|
||||
.changeset
|
||||
.zed
|
||||
.zed
|
||||
.env
|
||||
!.env.example
|
||||
+8
-29
@@ -12,7 +12,9 @@ APP_ENV=development
|
||||
APP_ORIGIN=http://localhost:3030
|
||||
ELECTRIC_ORIGIN=http://localhost:3060
|
||||
NODE_ENV=development
|
||||
V3_ENABLED=true
|
||||
|
||||
# Set this to UTC because Node.js uses the system timezone
|
||||
TZ="UTC"
|
||||
|
||||
# Redis is used for the v3 queuing and v2 concurrency control
|
||||
REDIS_HOST="localhost"
|
||||
@@ -22,6 +24,9 @@ REDIS_TLS_DISABLED="true"
|
||||
DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:3030/otel"
|
||||
DEV_OTEL_BATCH_PROCESSING_ENABLED="0"
|
||||
|
||||
# When the domain is set to `localhost` the CLI deploy command will only --load the image by default and not --push it
|
||||
DEPLOY_REGISTRY_HOST=localhost:5000
|
||||
|
||||
# OPTIONAL VARIABLES
|
||||
# This is used for validating emails that are allowed to log in. Every email that do not match this regex will be rejected.
|
||||
# WHITELISTED_EMAILS="authorized@yahoo\.com|authorized@gmail\.com"
|
||||
@@ -60,34 +65,11 @@ DEV_OTEL_BATCH_PROCESSING_ENABLED="0"
|
||||
# FROM_EMAIL=
|
||||
# REPLY_TO_EMAIL=
|
||||
|
||||
# Remove the following line to enable logging telemetry traces to the console
|
||||
LOG_TELEMETRY="false"
|
||||
|
||||
# CLOUD VARIABLES
|
||||
POSTHOG_PROJECT_KEY=
|
||||
PLAIN_API_KEY=
|
||||
CLOUD_AIRTABLE_CLIENT_ID=
|
||||
CLOUD_AIRTABLE_CLIENT_SECRET=
|
||||
CLOUD_GITHUB_CLIENT_ID=
|
||||
CLOUD_GITHUB_CLIENT_SECRET=
|
||||
CLOUD_LINEAR_CLIENT_ID=
|
||||
CLOUD_LINEAR_CLIENT_SECRET=
|
||||
CLOUD_SLACK_APP_HOST=
|
||||
CLOUD_SLACK_CLIENT_ID=
|
||||
CLOUD_SLACK_CLIENT_SECRET=
|
||||
|
||||
# v3 variables
|
||||
PROVIDER_SECRET=provider-secret # generate the actual secret with `openssl rand -hex 32`
|
||||
COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl rand -hex 32`
|
||||
|
||||
# Uncomment the following line to enable the registry proxy
|
||||
# ENABLE_REGISTRY_PROXY=true
|
||||
# DEPOT_ORG_ID=<Depot org id>
|
||||
# DEPOT_TOKEN=<Depot org token>
|
||||
# DEPOT_PROJECT_ID=<Depot project id>
|
||||
# DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry
|
||||
# CONTAINER_REGISTRY_ORIGIN=<Container registry origin e.g. https://registry.digitalocean.com>
|
||||
# CONTAINER_REGISTRY_USERNAME=<Container registry username e.g. Digital ocean email address>
|
||||
# CONTAINER_REGISTRY_PASSWORD=<Container registry password e.g. Digital ocean PAT>
|
||||
# DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318"
|
||||
# These are needed for the object store (for handling large payloads/outputs)
|
||||
# OBJECT_STORE_BASE_URL="https://{bucket}.{accountId}.r2.cloudflarestorage.com"
|
||||
@@ -97,8 +79,5 @@ COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl
|
||||
|
||||
# These control the server-side internal telemetry
|
||||
# INTERNAL_OTEL_TRACE_EXPORTER_URL=<URL to send traces to>
|
||||
# INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADER_NAME=<Header name for the auth token>
|
||||
# INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADER_VALUE=<Auth token value>
|
||||
# INTERNAL_OTEL_TRACE_LOGGING_ENABLED=1
|
||||
# INTERNAL_OTEL_TRACE_SAMPING_RATE=20 # this means 1/20 traces or 5% of traces will be sampled (sampled = recorded)
|
||||
# INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED=0,
|
||||
# INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED=0,
|
||||
@@ -1,5 +1,8 @@
|
||||
name: "E2E"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- ".changeset/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
name: "🐳 Publish Webapp"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -43,13 +48,25 @@ jobs:
|
||||
ref_without_tag=ghcr.io/triggerdotdev/trigger.dev
|
||||
image_tags=$ref_without_tag:${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
# if tag is a semver, also tag it as v3
|
||||
# if tag is a semver, also tag it as v4
|
||||
if [[ "${{ steps.get_tag.outputs.is_semver }}" == true ]]; then
|
||||
image_tags=$image_tags,$ref_without_tag:v3
|
||||
# TODO: switch to v4 tag on GA
|
||||
image_tags=$image_tags,$ref_without_tag:v4-beta
|
||||
fi
|
||||
|
||||
echo "image_tags=${image_tags}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 📝 Set the build info
|
||||
id: set_build_info
|
||||
run: |
|
||||
tag=${{ steps.get_tag.outputs.tag }}
|
||||
if [[ "${{ steps.get_tag.outputs.is_semver }}" == true ]]; then
|
||||
echo "BUILD_APP_VERSION=${tag}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "BUILD_GIT_SHA=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "BUILD_GIT_REF_NAME=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "BUILD_TIMESTAMP_SECONDS=$(date +%s)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -64,3 +81,13 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.set_tags.outputs.image_tags }}
|
||||
push: true
|
||||
build-args: |
|
||||
BUILD_APP_VERSION=${{ steps.set_build_info.outputs.BUILD_APP_VERSION }}
|
||||
BUILD_GIT_SHA=${{ steps.set_build_info.outputs.BUILD_GIT_SHA }}
|
||||
BUILD_GIT_REF_NAME=${{ steps.set_build_info.outputs.BUILD_GIT_REF_NAME }}
|
||||
BUILD_TIMESTAMP_SECONDS=${{ steps.set_build_info.outputs.BUILD_TIMESTAMP_SECONDS }}
|
||||
SENTRY_RELEASE=${{ steps.set_build_info.outputs.BUILD_GIT_SHA }}
|
||||
SENTRY_ORG=triggerdev
|
||||
SENTRY_PROJECT=trigger-cloud
|
||||
secrets: |
|
||||
sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
+26
-32
@@ -1,4 +1,4 @@
|
||||
name: "⚒️ Publish Worker RE2"
|
||||
name: "⚒️ Publish Worker (v4)"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
- "re2-prod-*"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
@@ -35,6 +36,9 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- name: 🏭 Setup Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: ⬇️ Checkout git repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -49,26 +53,26 @@ jobs:
|
||||
fi
|
||||
echo "repo=${repo}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- id: get_tag
|
||||
- name: "#️⃣ Get image tag"
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
with:
|
||||
tag: ${{ inputs.image_tag }}
|
||||
|
||||
- name: 🐋 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# ..to avoid rate limits when pulling images
|
||||
- name: 🐳 Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 🚢 Build Container Image
|
||||
- name: 📛 Set tags to push
|
||||
id: set_tags
|
||||
run: |
|
||||
docker build -t infra_image -f ./apps/${{ matrix.package }}/Containerfile .
|
||||
ref_without_tag=ghcr.io/triggerdotdev/${{ steps.get_repository.outputs.repo }}
|
||||
image_tags=$ref_without_tag:${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
# if tag is a semver, also tag it as v4
|
||||
if [[ "${{ steps.get_tag.outputs.is_semver }}" == true ]]; then
|
||||
# TODO: switch to v4 tag on GA
|
||||
image_tags=$image_tags,$ref_without_tag:v4-beta
|
||||
fi
|
||||
|
||||
echo "image_tags=${image_tags}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ..to push image
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -76,20 +80,10 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐙 Push to GitHub Container Registry
|
||||
run: |
|
||||
docker tag infra_image "$REGISTRY/$REPOSITORY:$IMAGE_TAG"
|
||||
docker push "$REGISTRY/$REPOSITORY:$IMAGE_TAG"
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: ${{ steps.get_repository.outputs.repo }}
|
||||
IMAGE_TAG: ${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
- name: 🐙 Push 'v3' tag to GitHub Container Registry
|
||||
if: steps.get_tag.outputs.is_semver == 'true'
|
||||
run: |
|
||||
docker tag infra_image "$REGISTRY/$REPOSITORY:v3"
|
||||
docker push "$REGISTRY/$REPOSITORY:v3"
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: ${{ steps.get_repository.outputs.repo }}
|
||||
- name: 🐳 Build image and push to GitHub Container Registry
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
file: ./apps/${{ matrix.package }}/Containerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.set_tags.outputs.image_tags }}
|
||||
push: true
|
||||
@@ -77,11 +77,11 @@ jobs:
|
||||
REPOSITORY: ${{ steps.get_repository.outputs.repo }}
|
||||
IMAGE_TAG: ${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
- name: 🐙 Push 'v3' tag to GitHub Container Registry
|
||||
if: steps.get_tag.outputs.is_semver == 'true'
|
||||
run: |
|
||||
docker tag infra_image "$REGISTRY/$REPOSITORY:v3"
|
||||
docker push "$REGISTRY/$REPOSITORY:v3"
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: ${{ steps.get_repository.outputs.repo }}
|
||||
# - name: 🐙 Push 'v3' tag to GitHub Container Registry
|
||||
# if: steps.get_tag.outputs.is_semver == 'true'
|
||||
# run: |
|
||||
# docker tag infra_image "$REGISTRY/$REPOSITORY:v3"
|
||||
# docker push "$REGISTRY/$REPOSITORY:v3"
|
||||
# env:
|
||||
# REGISTRY: ghcr.io/triggerdotdev
|
||||
# REPOSITORY: ${{ steps.get_repository.outputs.repo }}
|
||||
|
||||
@@ -68,3 +68,10 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
image_tag: ${{ inputs.image_tag }}
|
||||
|
||||
publish-worker-v4:
|
||||
needs: [typecheck]
|
||||
uses: ./.github/workflows/publish-worker-v4.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_tag: ${{ inputs.image_tag }}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
name: 🧭 Helm Chart Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'helm-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
chart_version:
|
||||
description: 'Chart version to release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
CHART_NAME: trigger
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: "3.18.3"
|
||||
|
||||
- name: Build dependencies
|
||||
run: helm dependency build ./hosting/k8s/helm/
|
||||
|
||||
- name: Extract dependency charts
|
||||
run: |
|
||||
cd ./hosting/k8s/helm/
|
||||
for file in ./charts/*.tgz; do echo "Extracting $file"; tar -xzf "$file" -C ./charts; done
|
||||
|
||||
- name: Lint Helm Chart
|
||||
run: |
|
||||
helm lint ./hosting/k8s/helm/
|
||||
|
||||
- name: Render templates
|
||||
run: |
|
||||
helm template test-release ./hosting/k8s/helm/ \
|
||||
--values ./hosting/k8s/helm/values.yaml \
|
||||
--output-dir ./helm-output
|
||||
|
||||
- name: Validate manifests
|
||||
uses: docker://ghcr.io/yannh/kubeconform:v0.7.0
|
||||
with:
|
||||
entrypoint: '/kubeconform'
|
||||
args: "-summary -output json ./helm-output"
|
||||
|
||||
release:
|
||||
needs: lint-and-test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # for gh-release
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: "3.18.3"
|
||||
|
||||
- name: Build dependencies
|
||||
run: helm dependency build ./hosting/k8s/helm/
|
||||
|
||||
- name: Extract dependency charts
|
||||
run: |
|
||||
cd ./hosting/k8s/helm/
|
||||
for file in ./charts/*.tgz; do echo "Extracting $file"; tar -xzf "$file" -C ./charts; done
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract version from tag or input
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION="${{ github.event.inputs.chart_version }}"
|
||||
else
|
||||
VERSION="${{ github.ref_name }}"
|
||||
VERSION="${VERSION#helm-v}"
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Releasing version: $VERSION"
|
||||
|
||||
- name: Check Chart.yaml version matches release version
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
CHART_VERSION=$(grep '^version:' ./hosting/k8s/helm/Chart.yaml | awk '{print $2}')
|
||||
echo "Chart.yaml version: $CHART_VERSION"
|
||||
echo "Release version: $VERSION"
|
||||
if [ "$CHART_VERSION" != "$VERSION" ]; then
|
||||
echo "❌ Chart.yaml version does not match release version!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Chart.yaml version matches release version."
|
||||
|
||||
- name: Package Helm Chart
|
||||
run: |
|
||||
helm package ./hosting/k8s/helm/ --destination /tmp/
|
||||
|
||||
- name: Push Helm Chart to GHCR
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
CHART_PACKAGE="/tmp/${{ env.CHART_NAME }}-${VERSION}.tgz"
|
||||
|
||||
# Push to GHCR OCI registry
|
||||
helm push "$CHART_PACKAGE" "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts"
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: github.event_name == 'push'
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: "Helm Chart ${{ steps.version.outputs.version }}"
|
||||
body: |
|
||||
### Installation
|
||||
```bash
|
||||
helm upgrade --install trigger \
|
||||
oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts/${{ env.CHART_NAME }} \
|
||||
--version "${{ steps.version.outputs.version }}"
|
||||
```
|
||||
|
||||
### Changes
|
||||
See commit history for detailed changes in this release.
|
||||
files: |
|
||||
/tmp/${{ env.CHART_NAME }}-${{ steps.version.outputs.version }}.tgz
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
@@ -1,5 +1,8 @@
|
||||
name: 🦋 Changesets Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -18,6 +21,10 @@ jobs:
|
||||
release:
|
||||
name: 🦋 Changesets Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
pull-requests: write
|
||||
if: github.repository == 'triggerdotdev/trigger.dev'
|
||||
outputs:
|
||||
published: ${{ steps.changesets.outputs.published }}
|
||||
@@ -89,13 +96,3 @@ jobs:
|
||||
run: |
|
||||
package_version=$(echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[0].version')
|
||||
echo "package_version=${package_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
publish:
|
||||
needs: release
|
||||
uses: ./.github/workflows/publish.yml
|
||||
secrets: inherit
|
||||
# if: needs.release.outputs.published == 'true'
|
||||
# disable automatic publishing for now
|
||||
if: false
|
||||
with:
|
||||
image_tag: v${{ needs.release.outputs.published_package_version }}
|
||||
|
||||
@@ -3,6 +3,9 @@ name: "ʦ TypeScript"
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
name: "🧪 Unit Tests: Internal"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests: Internal"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
shardTotal: [8]
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
SHARD_INDEX: ${{ matrix.shardIndex }}
|
||||
SHARD_TOTAL: ${{ matrix.shardTotal }}
|
||||
steps:
|
||||
- name: 🔧 Disable IPv6
|
||||
run: |
|
||||
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
||||
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
||||
sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1
|
||||
|
||||
- name: 🔧 Configure docker address pool
|
||||
run: |
|
||||
CONFIG='{
|
||||
"default-address-pools" : [
|
||||
{
|
||||
"base" : "172.17.0.0/12",
|
||||
"size" : 20
|
||||
},
|
||||
{
|
||||
"base" : "192.168.0.0/16",
|
||||
"size" : 24
|
||||
}
|
||||
]
|
||||
}'
|
||||
mkdir -p /etc/docker
|
||||
echo "$CONFIG" | sudo tee /etc/docker/daemon.json
|
||||
|
||||
- name: 🔧 Restart docker daemon
|
||||
run: sudo systemctl restart docker
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
# ..to avoid rate limits when pulling images
|
||||
- name: 🐳 Login to DockerHub
|
||||
if: ${{ env.DOCKERHUB_USERNAME }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🐳 Skipping DockerHub login (no secrets available)
|
||||
if: ${{ !env.DOCKERHUB_USERNAME }}
|
||||
run: echo "DockerHub login skipped because secrets are not available."
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🧪 Run Internal Unit Tests
|
||||
run: pnpm run test:internal --reporter=default --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
- name: Gather all reports
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
mkdir -p .vitest-reports
|
||||
find . -type f -path '*/.vitest-reports/blob-*.json' \
|
||||
-exec bash -c 'src="$1"; basename=$(basename "$src"); pkg=$(dirname "$src" | sed "s|^\./||;s|/\.vitest-reports$||;s|/|_|g"); cp "$src" ".vitest-reports/${pkg}-${basename}"' _ {} \;
|
||||
|
||||
- name: Upload blob reports to GitHub Actions Artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: internal-blob-report-${{ matrix.shardIndex }}
|
||||
path: .vitest-reports/*
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
merge-reports:
|
||||
name: "📊 Merge Reports"
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [unitTests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
# no cache enabled, we're not installing deps
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: .vitest-reports
|
||||
pattern: internal-blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge reports
|
||||
run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests
|
||||
@@ -0,0 +1,130 @@
|
||||
name: "🧪 Unit Tests: Packages"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests: Packages"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
shardIndex: [1]
|
||||
shardTotal: [1]
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
SHARD_INDEX: ${{ matrix.shardIndex }}
|
||||
SHARD_TOTAL: ${{ matrix.shardTotal }}
|
||||
steps:
|
||||
- name: 🔧 Disable IPv6
|
||||
run: |
|
||||
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
||||
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
||||
sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1
|
||||
|
||||
- name: 🔧 Configure docker address pool
|
||||
run: |
|
||||
CONFIG='{
|
||||
"default-address-pools" : [
|
||||
{
|
||||
"base" : "172.17.0.0/12",
|
||||
"size" : 20
|
||||
},
|
||||
{
|
||||
"base" : "192.168.0.0/16",
|
||||
"size" : 24
|
||||
}
|
||||
]
|
||||
}'
|
||||
mkdir -p /etc/docker
|
||||
echo "$CONFIG" | sudo tee /etc/docker/daemon.json
|
||||
|
||||
- name: 🔧 Restart docker daemon
|
||||
run: sudo systemctl restart docker
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
# ..to avoid rate limits when pulling images
|
||||
- name: 🐳 Login to DockerHub
|
||||
if: ${{ env.DOCKERHUB_USERNAME }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🐳 Skipping DockerHub login (no secrets available)
|
||||
if: ${{ !env.DOCKERHUB_USERNAME }}
|
||||
run: echo "DockerHub login skipped because secrets are not available."
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🧪 Run Package Unit Tests
|
||||
run: pnpm run test:packages --reporter=default --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
- name: Gather all reports
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
mkdir -p .vitest-reports
|
||||
find . -type f -path '*/.vitest-reports/blob-*.json' \
|
||||
-exec bash -c 'src="$1"; basename=$(basename "$src"); pkg=$(dirname "$src" | sed "s|^\./||;s|/\.vitest-reports$||;s|/|_|g"); cp "$src" ".vitest-reports/${pkg}-${basename}"' _ {} \;
|
||||
|
||||
- name: Upload blob reports to GitHub Actions Artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packages-blob-report-${{ matrix.shardIndex }}
|
||||
path: .vitest-reports/*
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
merge-reports:
|
||||
name: "📊 Merge Reports"
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [unitTests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
# no cache enabled, we're not installing deps
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: .vitest-reports
|
||||
pattern: packages-blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge reports
|
||||
run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests
|
||||
@@ -0,0 +1,138 @@
|
||||
name: "🧪 Unit Tests: Webapp"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests: Webapp"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
shardTotal: [8]
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
SHARD_INDEX: ${{ matrix.shardIndex }}
|
||||
SHARD_TOTAL: ${{ matrix.shardTotal }}
|
||||
steps:
|
||||
- name: 🔧 Disable IPv6
|
||||
run: |
|
||||
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
||||
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
||||
sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1
|
||||
|
||||
- name: 🔧 Configure docker address pool
|
||||
run: |
|
||||
CONFIG='{
|
||||
"default-address-pools" : [
|
||||
{
|
||||
"base" : "172.17.0.0/12",
|
||||
"size" : 20
|
||||
},
|
||||
{
|
||||
"base" : "192.168.0.0/16",
|
||||
"size" : 24
|
||||
}
|
||||
]
|
||||
}'
|
||||
mkdir -p /etc/docker
|
||||
echo "$CONFIG" | sudo tee /etc/docker/daemon.json
|
||||
|
||||
- name: 🔧 Restart docker daemon
|
||||
run: sudo systemctl restart docker
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
# ..to avoid rate limits when pulling images
|
||||
- name: 🐳 Login to DockerHub
|
||||
if: ${{ env.DOCKERHUB_USERNAME }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🐳 Skipping DockerHub login (no secrets available)
|
||||
if: ${{ !env.DOCKERHUB_USERNAME }}
|
||||
run: echo "DockerHub login skipped because secrets are not available."
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🧪 Run Webapp Unit Tests
|
||||
run: pnpm run test:webapp --reporter=default --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
SESSION_SECRET: "secret"
|
||||
MAGIC_LINK_SECRET: "secret"
|
||||
ENCRYPTION_KEY: "secret"
|
||||
DEPLOY_REGISTRY_HOST: "docker.io"
|
||||
CLICKHOUSE_URL: "http://default:password@localhost:8123"
|
||||
|
||||
- name: Gather all reports
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
mkdir -p .vitest-reports
|
||||
find . -type f -path '*/.vitest-reports/blob-*.json' \
|
||||
-exec bash -c 'src="$1"; basename=$(basename "$src"); pkg=$(dirname "$src" | sed "s|^\./||;s|/\.vitest-reports$||;s|/|_|g"); cp "$src" ".vitest-reports/${pkg}-${basename}"' _ {} \;
|
||||
|
||||
- name: Upload blob reports to GitHub Actions Artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: webapp-blob-report-${{ matrix.shardIndex }}
|
||||
path: .vitest-reports/*
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
merge-reports:
|
||||
name: "📊 Merge Reports"
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [unitTests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
# no cache enabled, we're not installing deps
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: .vitest-reports
|
||||
pattern: webapp-blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge reports
|
||||
run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests
|
||||
@@ -1,53 +1,18 @@
|
||||
name: "🧪 Unit Tests"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
# ..to avoid rate limits when pulling images
|
||||
- name: 🐳 Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🧪 Run Webapp Unit Tests
|
||||
run: pnpm run test:webapp
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
SESSION_SECRET: "secret"
|
||||
MAGIC_LINK_SECRET: "secret"
|
||||
ENCRYPTION_KEY: "secret"
|
||||
|
||||
- name: 🧪 Run Package Unit Tests
|
||||
run: pnpm run test:packages
|
||||
|
||||
- name: 🧪 Run Internal Unit Tests
|
||||
run: pnpm run test:internal
|
||||
webapp:
|
||||
uses: ./.github/workflows/unit-tests-webapp.yml
|
||||
secrets: inherit
|
||||
packages:
|
||||
uses: ./.github/workflows/unit-tests-packages.yml
|
||||
secrets: inherit
|
||||
internal:
|
||||
uses: ./.github/workflows/unit-tests-internal.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -63,3 +63,4 @@ apps/**/public/build
|
||||
/packages/core/src/package.json
|
||||
/packages/trigger-sdk/src/package.json
|
||||
/packages/python/src/package.json
|
||||
.claude
|
||||
Vendored
+3
-3
@@ -25,8 +25,8 @@
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug fairDequeuingStrategy.test.ts",
|
||||
"command": "pnpm run test -t FairDequeuingStrategy",
|
||||
"name": "Debug triggerTask.test.ts",
|
||||
"command": "pnpm run test --run ./test/engine/triggerTask.test.ts",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"cwd": "${workspaceFolder}/apps/webapp",
|
||||
"sourceMaps": true
|
||||
@@ -146,7 +146,7 @@
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug RunQueue tests",
|
||||
"command": "pnpm run test ./src/run-queue/index.test.ts",
|
||||
"command": "pnpm run test ./src/run-queue/index.test.ts --run",
|
||||
"cwd": "${workspaceFolder}/internal-packages/run-engine",
|
||||
"sourceMaps": true
|
||||
},
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Guidance for Coding Agents
|
||||
|
||||
This repository is a pnpm monorepo managed with Turbo. It contains multiple apps and packages that make up the Trigger.dev platform and SDK.
|
||||
|
||||
## Repository layout
|
||||
- `apps/webapp` – Remix application that serves as the main API and dashboard.
|
||||
- `apps/supervisor` – Node application for executing built tasks.
|
||||
- `packages/*` – Published packages such as `@trigger.dev/sdk`, the CLI (`trigger.dev`), and shared libraries.
|
||||
- `internal-packages/*` – Internal-only packages used by the webapp and other apps.
|
||||
- `references/*` – Example projects for manual testing and development of new features.
|
||||
- `ai/references` – Contains additional documentation including an overview (`repo.md`) and testing guidelines (`tests.md`).
|
||||
|
||||
See `ai/references/repo.md` for a more complete explanation of the workspaces.
|
||||
|
||||
## Development setup
|
||||
1. Install dependencies with `pnpm i` (pnpm `8.15.5` and Node.js `20.11.1` are required).
|
||||
2. Copy `.env.example` to `.env` and generate a random 16 byte hex string for `ENCRYPTION_KEY` (`openssl rand -hex 16`). Update other secrets if needed.
|
||||
3. Start the local services with Docker:
|
||||
```bash
|
||||
pnpm run docker
|
||||
```
|
||||
4. Run database migrations:
|
||||
```bash
|
||||
pnpm run db:migrate
|
||||
```
|
||||
5. Build the webapp, CLI and SDK packages:
|
||||
```bash
|
||||
pnpm run build --filter webapp && pnpm run build --filter trigger.dev && pnpm run build --filter @trigger.dev/sdk
|
||||
```
|
||||
6. Launch the development server:
|
||||
```bash
|
||||
pnpm run dev --filter webapp
|
||||
```
|
||||
The webapp runs on <http://localhost:3030>.
|
||||
|
||||
For full setup instructions see `CONTRIBUTING.md`.
|
||||
|
||||
## Running tests
|
||||
- Unit tests use **vitest**. Run all tests:
|
||||
```bash
|
||||
pnpm run test
|
||||
```
|
||||
- Run tests for a specific workspace (example for `webapp`):
|
||||
```bash
|
||||
pnpm run test --filter webapp
|
||||
```
|
||||
- Prefer running a single test file from within its directory:
|
||||
```bash
|
||||
cd apps/webapp
|
||||
pnpm run test ./src/components/Button.test.ts
|
||||
```
|
||||
If packages in that workspace need to be built first, run `pnpm run build --filter webapp`.
|
||||
|
||||
Refer to `ai/references/tests.md` for details on writing tests. Tests should avoid mocks or stubs and use the helpers from `@internal/testcontainers` when Redis or Postgres are needed.
|
||||
|
||||
## Coding style
|
||||
- Formatting is enforced using Prettier. Run `pnpm run format` before committing.
|
||||
- Follow the existing project conventions. Test files live beside the files under test and use descriptive `describe` and `it` blocks.
|
||||
- Do not commit directly to the `main` branch. All changes should be made in a separate branch and go through a pull request.
|
||||
|
||||
## Additional docs
|
||||
- The root `README.md` describes Trigger.dev and links to documentation.
|
||||
- The `docs` workspace contains our documentation site, which can be run locally with:
|
||||
```bash
|
||||
pnpm run dev --filter docs
|
||||
```
|
||||
- `references/README.md` explains how to create new reference projects for manual testing.
|
||||
|
||||
+6
-20
@@ -30,28 +30,14 @@ Please follow the best-practice of adding changesets in the same commit as the c
|
||||
|
||||
## Snapshot instructions
|
||||
|
||||
!MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW!
|
||||
1. Delete the `.changeset/pre.json` file (if it exists)
|
||||
|
||||
1. Add changesets as usual
|
||||
2. Do a temporary commit (do NOT push this, you should undo it after)
|
||||
|
||||
```sh
|
||||
pnpm run changeset:add
|
||||
```
|
||||
3. Copy the `GITHUB_TOKEN` line from the .env file
|
||||
|
||||
2. Create a snapshot version (replace "prerelease" with your tag)
|
||||
4. Run `GITHUB_TOKEN=github_pat_12345 ./scripts/publish-prerelease.sh re2`
|
||||
|
||||
```sh
|
||||
pnpm exec changeset version --snapshot prerelease
|
||||
```
|
||||
Make sure to replace the token with yours. `re2` is the tag that will be used for the pre-release.
|
||||
|
||||
3. Build the packages:
|
||||
|
||||
```sh
|
||||
pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
```
|
||||
|
||||
4. Publish the snapshot (replace "dev" with your tag)
|
||||
|
||||
```sh
|
||||
pnpm exec changeset publish --no-git-tag --snapshot --tag prerelease
|
||||
```
|
||||
5. Undo the commit where you deleted the pre.json file.
|
||||
|
||||
+6
-10
@@ -62,15 +62,13 @@ branch are tagged into a release periodically.
|
||||
pnpm run docker
|
||||
```
|
||||
|
||||
This will also start and run a local instance of [pgAdmin](https://www.pgadmin.org/) on [localhost:5480](http://localhost:5480), preconfigured with email `admin@example.com` and pwd `admin`. Then use `postgres` as the password to the Trigger.dev server.
|
||||
|
||||
9. Migrate the database
|
||||
```
|
||||
pnpm run db:migrate
|
||||
```
|
||||
10. Build the server app
|
||||
10. Build everything
|
||||
```
|
||||
pnpm run build --filter webapp
|
||||
pnpm run build --filter webapp && pnpm run build --filter trigger.dev && pnpm run build --filter @trigger.dev/sdk
|
||||
```
|
||||
11. Run the app. See the section below.
|
||||
|
||||
@@ -94,13 +92,11 @@ We use the `<root>/references/v3-catalog` subdirectory as a staging ground for t
|
||||
|
||||
First, make sure you are running the webapp according to the instructions above. Then:
|
||||
|
||||
1. In Postgres go to the "Organizations" table and on your org set the `v3Enabled` column to `true`.
|
||||
1. Visit http://localhost:3030 in your browser and create a new V3 project called "v3-catalog".
|
||||
|
||||
2. Visit http://localhost:3030 in your browser and create a new V3 project called "v3-catalog". If you don't see an option for V3, you haven't set the `v3Enabled` flag to true.
|
||||
2. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`.
|
||||
|
||||
3. In Postgres go to the "Projects" table and for the project you create change the `externalRef` to `yubjwjsfkxnylobaqvqz`.
|
||||
|
||||
4. Build the CLI
|
||||
3. Build the CLI
|
||||
|
||||
```sh
|
||||
# Build the CLI
|
||||
@@ -109,7 +105,7 @@ pnpm run build --filter trigger.dev
|
||||
pnpm i
|
||||
```
|
||||
|
||||
5. Change into the `<root>/references/v3-catalog` directory and authorize the CLI to the local server:
|
||||
4. Change into the `<root>/references/v3-catalog` directory and authorize the CLI to the local server:
|
||||
|
||||
```sh
|
||||
cd references/v3-catalog
|
||||
|
||||
+24
-15
@@ -8,47 +8,56 @@ If you don't have Docker installed on your machine, you'll run into some complic
|
||||
|
||||
Below are the steps on how you can avoid that.
|
||||
|
||||
First you need to setup docker-compose as it is an underlying tool that this command: `pnpm run docker` fires behind the scene.
|
||||
First you need to setup docker compose as it is an underlying tool that this command: `pnpm run docker` fires behind the scene.
|
||||
|
||||
## Linux
|
||||
|
||||
To install Docker Compose on Linux Ubuntu via the terminal, you can follow these steps:
|
||||
To install Docker Compose on Linux Ubuntu, you can follow these steps:
|
||||
|
||||
1. Update the package index on your system by running the following command:
|
||||
1. Create the Docker config directory and cli-plugins subdirectory:
|
||||
|
||||
```shell
|
||||
sudo apt update
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins
|
||||
```
|
||||
|
||||
2. Install the required dependencies by running the following command:
|
||||
2. Download the Docker Compose plugin:
|
||||
|
||||
```shell
|
||||
sudo apt install curl
|
||||
curl -SL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o $DOCKER_CONFIG/cli-plugins/docker-compose
|
||||
```
|
||||
|
||||
3. Download the Docker Compose binary into the `/usr/local/bin` directory using the `curl` command:
|
||||
Note:
|
||||
|
||||
- To install for all users, replace `$DOCKER_CONFIG/cli-plugins` with `/usr/local/lib/docker/cli-plugins`
|
||||
|
||||
3. Set the appropriate permissions to make the Docker Compose plugin executable:
|
||||
|
||||
```shell
|
||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
|
||||
```
|
||||
|
||||
4. Set the appropriate permissions to make the `docker-compose` binary executable:
|
||||
If you installed for all users:
|
||||
|
||||
```shell
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
```
|
||||
|
||||
5. Verify that Docker Compose has been successfully installed by running the following command:
|
||||
4. Verify that Docker Compose has been successfully installed:
|
||||
|
||||
```shell
|
||||
docker-compose --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
This command should display the version information of Docker Compose without any errors.
|
||||
You should see output similar to:
|
||||
|
||||
After following these steps, you should have Docker Compose installed on your Ubuntu system, and you can use it by running `docker-compose` commands in the terminal.
|
||||
```
|
||||
Docker Compose version vX.Y.Z
|
||||
```
|
||||
|
||||
When you've verified that the `docker-compose` package is installed and you proceed to start Docker with `pnpm run docker`.
|
||||
After following these steps, you should have Docker Compose installed on your Ubuntu system, and you can use it by running `docker compose` commands in the terminal.
|
||||
|
||||
When you've verified that the `docker compose` package is installed and you proceed to start Docker with `pnpm run docker`.
|
||||
|
||||
You'll probably get an error similar to the one below:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
@@ -7,12 +7,11 @@ MANAGED_WORKER_SECRET=managed-secret
|
||||
# Point this at the webapp in prod
|
||||
TRIGGER_API_URL=http://localhost:3030
|
||||
|
||||
# Point this at the OTel collector in prod
|
||||
# Point this at the webapp or an OTel collector in prod
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:3030/otel
|
||||
# Use this on macOS
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:3030/otel
|
||||
|
||||
# Optional settings
|
||||
DEBUG=1
|
||||
ENFORCE_MACHINE_PRESETS=1
|
||||
TRIGGER_DEQUEUE_INTERVAL_MS=1000
|
||||
@@ -19,6 +19,7 @@ COPY --from=pruner --chown=node:node /app/out/pnpm-workspace.yaml ./pnpm-workspa
|
||||
RUN corepack enable && corepack prepare --activate
|
||||
|
||||
FROM base AS deps-fetcher
|
||||
RUN apk add --no-cache python3-dev py3-setuptools make g++ gcc linux-headers
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch --frozen-lockfile
|
||||
|
||||
FROM deps-fetcher AS dev-deps
|
||||
|
||||
@@ -19,6 +19,8 @@ curl -sS \
|
||||
-d "{\"name\": \"$wg_name\"}"
|
||||
```
|
||||
|
||||
If the worker group is newly created, the response will include a `token` field. If the group already exists, no token is returned.
|
||||
|
||||
2. Create `.env` and set the worker token
|
||||
|
||||
```sh
|
||||
@@ -34,7 +36,7 @@ TRIGGER_WORKER_TOKEN=tr_wgt_...
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. Build CLI, then deploy a reference project
|
||||
4. Build CLI, then deploy a test project
|
||||
|
||||
```sh
|
||||
pnpm exec trigger deploy --self-hosted
|
||||
@@ -43,16 +45,36 @@ pnpm exec trigger deploy --self-hosted
|
||||
pnpm exec trigger deploy --self-hosted --network host
|
||||
```
|
||||
|
||||
## Additional worker groups
|
||||
## Worker group management
|
||||
|
||||
When adding more worker groups you might also want to make them the default for a specific project. This will allow you to test it without having to change the global default:
|
||||
### Shared variables
|
||||
|
||||
```sh
|
||||
api_url=http://localhost:3030
|
||||
admin_pat=tr_pat_... # edit this
|
||||
```
|
||||
|
||||
- These are used by all commands
|
||||
|
||||
### Create a worker group
|
||||
|
||||
```sh
|
||||
wg_name=my-worker
|
||||
|
||||
# edit these
|
||||
admin_pat=tr_pat_...
|
||||
curl -sS \
|
||||
-X POST \
|
||||
"$api_url/admin/api/v1/workers" \
|
||||
-H "Authorization: Bearer $admin_pat" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\": \"$wg_name\"}"
|
||||
```
|
||||
|
||||
- If the worker group already exists, no token will be returned
|
||||
|
||||
### Set a worker group as default for a project
|
||||
|
||||
```sh
|
||||
wg_name=my-worker
|
||||
project_id=clsw6q8wz...
|
||||
|
||||
curl -sS \
|
||||
@@ -60,8 +82,24 @@ curl -sS \
|
||||
"$api_url/admin/api/v1/workers" \
|
||||
-H "Authorization: Bearer $admin_pat" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"name\": \"$wg_name\",
|
||||
\"makeDefaultForProjectId\": \"$project_id\"
|
||||
}"
|
||||
-d "{\"name\": \"$wg_name\", \"projectId\": \"$project_id\", \"makeDefaultForProject\": true}"
|
||||
```
|
||||
|
||||
- If the worker group doesn't exist, yet it will be created
|
||||
- If the worker group already exists, it will be attached to the project as default. No token will be returned.
|
||||
|
||||
### Remove the default worker group from a project
|
||||
|
||||
```sh
|
||||
project_id=clsw6q8wz...
|
||||
|
||||
curl -sS \
|
||||
-X POST \
|
||||
"$api_url/admin/api/v1/workers" \
|
||||
-H "Authorization: Bearer $admin_pat" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"projectId\": \"$project_id\", \"removeDefaultFromProject\": true}"
|
||||
```
|
||||
|
||||
- The project will then use the global default again
|
||||
- When `removeDefaultFromProject: true` no other actions will be performed
|
||||
|
||||
@@ -15,17 +15,13 @@
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.0.0",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"dockerode": "^4.0.3",
|
||||
"nanoid": "^5.0.9",
|
||||
"dockerode": "^4.0.6",
|
||||
"prom-client": "^15.1.0",
|
||||
"socket.io": "4.7.4",
|
||||
"std-env": "^3.8.0",
|
||||
"tinyexec": "^0.3.1",
|
||||
"zod": "3.23.8"
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dockerode": "^3.3.33",
|
||||
"docker-api-ts": "^0.2.2",
|
||||
"vitest": "^1.4.0"
|
||||
"@types/dockerode": "^3.3.33"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ import { Informer } from "@kubernetes/client-node";
|
||||
import { ListPromise } from "@kubernetes/client-node";
|
||||
import { KubernetesObject } from "@kubernetes/client-node";
|
||||
import { assertExhaustive } from "@trigger.dev/core/utils";
|
||||
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
|
||||
export const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local";
|
||||
|
||||
const logger = new SimpleStructuredLogger("kubernetes-client");
|
||||
|
||||
export function createK8sApi() {
|
||||
const kubeConfig = getKubeConfig();
|
||||
|
||||
@@ -31,7 +34,7 @@ export function createK8sApi() {
|
||||
export type K8sApi = ReturnType<typeof createK8sApi>;
|
||||
|
||||
function getKubeConfig() {
|
||||
console.log("getKubeConfig()", { RUNTIME_ENV });
|
||||
logger.debug("getKubeConfig()", { RUNTIME_ENV });
|
||||
|
||||
const kubeConfig = new k8s.KubeConfig();
|
||||
|
||||
|
||||
+42
-16
@@ -6,14 +6,16 @@ import { AdditionalEnvVars, BoolEnv } from "./envUtil.js";
|
||||
const Env = z.object({
|
||||
// This will come from `spec.nodeName` in k8s
|
||||
TRIGGER_WORKER_INSTANCE_NAME: z.string().default(randomUUID()),
|
||||
TRIGGER_WORKER_HEARTBEAT_INTERVAL_SECONDS: z.coerce.number().default(30),
|
||||
|
||||
// Required settings
|
||||
TRIGGER_API_URL: z.string().url(),
|
||||
TRIGGER_WORKER_TOKEN: z.string(),
|
||||
TRIGGER_WORKER_TOKEN: z.string(), // accepts file:// path to read from a file
|
||||
MANAGED_WORKER_SECRET: z.string(),
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url(), // set on the runners
|
||||
|
||||
// Workload API settings (coordinator mode) - the workload API is what the run controller connects to
|
||||
TRIGGER_WORKLOAD_API_ENABLED: BoolEnv.default("true"),
|
||||
TRIGGER_WORKLOAD_API_ENABLED: BoolEnv.default(true),
|
||||
TRIGGER_WORKLOAD_API_PROTOCOL: z
|
||||
.string()
|
||||
.transform((s) => z.enum(["http", "https"]).parse(s.toLowerCase()))
|
||||
@@ -27,34 +29,57 @@ const Env = z.object({
|
||||
RUNNER_HEARTBEAT_INTERVAL_SECONDS: z.coerce.number().optional(),
|
||||
RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS: z.coerce.number().optional(),
|
||||
RUNNER_ADDITIONAL_ENV_VARS: AdditionalEnvVars, // optional (csv)
|
||||
RUNNER_DOCKER_AUTOREMOVE: BoolEnv.default(true),
|
||||
RUNNER_PRETTY_LOGS: BoolEnv.default(false),
|
||||
|
||||
// Dequeue settings (provider mode)
|
||||
TRIGGER_DEQUEUE_ENABLED: BoolEnv.default("true"),
|
||||
TRIGGER_DEQUEUE_INTERVAL_MS: z.coerce.number().int().default(1000),
|
||||
TRIGGER_DEQUEUE_ENABLED: BoolEnv.default(true),
|
||||
TRIGGER_DEQUEUE_INTERVAL_MS: z.coerce.number().int().default(250),
|
||||
TRIGGER_DEQUEUE_IDLE_INTERVAL_MS: z.coerce.number().int().default(1000),
|
||||
TRIGGER_DEQUEUE_MAX_RUN_COUNT: z.coerce.number().int().default(10),
|
||||
TRIGGER_DEQUEUE_MAX_CONSUMER_COUNT: z.coerce.number().int().default(1),
|
||||
|
||||
// Optional services
|
||||
TRIGGER_WARM_START_URL: z.string().optional(),
|
||||
TRIGGER_CHECKPOINT_URL: z.string().optional(),
|
||||
TRIGGER_METADATA_URL: z.string().optional(),
|
||||
|
||||
// Used by the workload manager, e.g docker/k8s
|
||||
DOCKER_NETWORK: z.string().default("host"),
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url(),
|
||||
ENFORCE_MACHINE_PRESETS: z.coerce.boolean().default(false),
|
||||
KUBERNETES_IMAGE_PULL_SECRETS: z.string().optional(), // csv
|
||||
|
||||
// Used by the resource monitor
|
||||
OVERRIDE_CPU_TOTAL: z.coerce.number().optional(),
|
||||
OVERRIDE_MEMORY_TOTAL_GB: z.coerce.number().optional(),
|
||||
RESOURCE_MONITOR_ENABLED: BoolEnv.default(false),
|
||||
RESOURCE_MONITOR_OVERRIDE_CPU_TOTAL: z.coerce.number().optional(),
|
||||
RESOURCE_MONITOR_OVERRIDE_MEMORY_TOTAL_GB: z.coerce.number().optional(),
|
||||
|
||||
// Kubernetes specific settings
|
||||
// Docker settings
|
||||
DOCKER_API_VERSION: z.string().default("v1.41"),
|
||||
DOCKER_PLATFORM: z.string().optional(), // e.g. linux/amd64, linux/arm64
|
||||
DOCKER_STRIP_IMAGE_DIGEST: BoolEnv.default(true),
|
||||
DOCKER_REGISTRY_USERNAME: z.string().optional(),
|
||||
DOCKER_REGISTRY_PASSWORD: z.string().optional(),
|
||||
DOCKER_REGISTRY_URL: z.string().optional(), // e.g. https://index.docker.io/v1
|
||||
DOCKER_ENFORCE_MACHINE_PRESETS: BoolEnv.default(true),
|
||||
DOCKER_AUTOREMOVE_EXITED_CONTAINERS: BoolEnv.default(true),
|
||||
/**
|
||||
* Network mode to use for all runners. Supported standard values are: `bridge`, `host`, `none`, and `container:<name|id>`.
|
||||
* Any other value is taken as a custom network's name to which all runners should connect to.
|
||||
*
|
||||
* Accepts a list of comma-separated values to attach to multiple networks. Additional networks are interpreted as network names and will be attached after container creation.
|
||||
*
|
||||
* **WARNING**: Specifying multiple networks will slightly increase startup times.
|
||||
*
|
||||
* @default "host"
|
||||
*/
|
||||
DOCKER_RUNNER_NETWORKS: z.string().default("host"),
|
||||
|
||||
// Kubernetes settings
|
||||
KUBERNETES_FORCE_ENABLED: BoolEnv.default(false),
|
||||
KUBERNETES_NAMESPACE: z.string().default("default"),
|
||||
KUBERNETES_WORKER_NODETYPE_LABEL: z.string().default("v4-worker"),
|
||||
EPHEMERAL_STORAGE_SIZE_LIMIT: z.string().default("10Gi"),
|
||||
EPHEMERAL_STORAGE_SIZE_REQUEST: z.string().default("2Gi"),
|
||||
KUBERNETES_IMAGE_PULL_SECRETS: z.string().optional(), // csv
|
||||
KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT: z.string().default("10Gi"),
|
||||
KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST: z.string().default("2Gi"),
|
||||
|
||||
// Placement tags settings
|
||||
PLACEMENT_TAGS_ENABLED: BoolEnv.default(false),
|
||||
PLACEMENT_TAGS_PREFIX: z.string().default("node.cluster.x-k8s.io"),
|
||||
|
||||
// Metrics
|
||||
METRICS_ENABLED: BoolEnv.default(true),
|
||||
@@ -73,6 +98,7 @@ const Env = z.object({
|
||||
|
||||
// Debug
|
||||
DEBUG: BoolEnv.default(false),
|
||||
SEND_RUN_DEBUG_LOGS: BoolEnv.default(false),
|
||||
});
|
||||
|
||||
export const env = Env.parse(stdEnv);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
|
||||
export const BoolEnv = z.preprocess((val) => {
|
||||
const logger = new SimpleStructuredLogger("env-util");
|
||||
|
||||
const baseBoolEnv = z.preprocess((val) => {
|
||||
if (typeof val !== "string") {
|
||||
return val;
|
||||
}
|
||||
@@ -8,6 +11,11 @@ export const BoolEnv = z.preprocess((val) => {
|
||||
return ["true", "1"].includes(val.toLowerCase().trim());
|
||||
}, z.boolean());
|
||||
|
||||
// Create a type-safe version that only accepts boolean defaults
|
||||
export const BoolEnv = baseBoolEnv as Omit<typeof baseBoolEnv, "default"> & {
|
||||
default: (value: boolean) => z.ZodDefault<typeof baseBoolEnv>;
|
||||
};
|
||||
|
||||
export const AdditionalEnvVars = z.preprocess((val) => {
|
||||
if (typeof val !== "string") {
|
||||
return val;
|
||||
@@ -33,7 +41,7 @@ export const AdditionalEnvVars = z.preprocess((val) => {
|
||||
// Return undefined if no valid key-value pairs were found
|
||||
return Object.keys(result).length === 0 ? undefined : result;
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse additional env vars", { error, val });
|
||||
logger.warn("Failed to parse additional env vars", { error, val });
|
||||
return undefined;
|
||||
}
|
||||
}, z.record(z.string(), z.string()).optional());
|
||||
|
||||
@@ -9,6 +9,7 @@ import { type DequeuedMessage } from "@trigger.dev/core/v3";
|
||||
import {
|
||||
DockerResourceMonitor,
|
||||
KubernetesResourceMonitor,
|
||||
NoopResourceMonitor,
|
||||
type ResourceMonitor,
|
||||
} from "./resourceMonitor.js";
|
||||
import { KubernetesWorkloadManager } from "./workloadManager/kubernetes.js";
|
||||
@@ -23,6 +24,7 @@ import { collectDefaultMetrics } from "prom-client";
|
||||
import { register } from "./metrics.js";
|
||||
import { PodCleaner } from "./services/podCleaner.js";
|
||||
import { FailedPodHandler } from "./services/failedPodHandler.js";
|
||||
import { getWorkerToken } from "./workerToken.js";
|
||||
|
||||
if (env.METRICS_COLLECT_DEFAULTS) {
|
||||
collectDefaultMetrics({ register });
|
||||
@@ -33,7 +35,7 @@ class ManagedSupervisor {
|
||||
private readonly metricsServer?: HttpServer;
|
||||
private readonly workloadServer: WorkloadServer;
|
||||
private readonly workloadManager: WorkloadManager;
|
||||
private readonly logger = new SimpleStructuredLogger("managed-worker");
|
||||
private readonly logger = new SimpleStructuredLogger("managed-supervisor");
|
||||
private readonly resourceMonitor: ResourceMonitor;
|
||||
private readonly checkpointClient?: CheckpointClient;
|
||||
|
||||
@@ -47,11 +49,11 @@ class ManagedSupervisor {
|
||||
const { TRIGGER_WORKER_TOKEN, MANAGED_WORKER_SECRET, ...envWithoutSecrets } = env;
|
||||
|
||||
if (env.DEBUG) {
|
||||
console.debug("[ManagedSupervisor] Starting up", { envWithoutSecrets });
|
||||
this.logger.debug("Starting up", { envWithoutSecrets });
|
||||
}
|
||||
|
||||
if (this.warmStartUrl) {
|
||||
this.logger.log("[ManagedWorker] 🔥 Warm starts enabled", {
|
||||
this.logger.log("🔥 Warm starts enabled", {
|
||||
warmStartUrl: this.warmStartUrl,
|
||||
});
|
||||
}
|
||||
@@ -66,12 +68,22 @@ class ManagedSupervisor {
|
||||
heartbeatIntervalSeconds: env.RUNNER_HEARTBEAT_INTERVAL_SECONDS,
|
||||
snapshotPollIntervalSeconds: env.RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS,
|
||||
additionalEnvVars: env.RUNNER_ADDITIONAL_ENV_VARS,
|
||||
dockerAutoremove: env.RUNNER_DOCKER_AUTOREMOVE,
|
||||
dockerAutoremove: env.DOCKER_AUTOREMOVE_EXITED_CONTAINERS,
|
||||
} satisfies WorkloadManagerOptions;
|
||||
|
||||
this.resourceMonitor = env.RESOURCE_MONITOR_ENABLED
|
||||
? this.isKubernetes
|
||||
? new KubernetesResourceMonitor(createK8sApi(), env.TRIGGER_WORKER_INSTANCE_NAME)
|
||||
: new DockerResourceMonitor(new Docker())
|
||||
: new NoopResourceMonitor();
|
||||
|
||||
this.workloadManager = this.isKubernetes
|
||||
? new KubernetesWorkloadManager(workloadManagerOptions)
|
||||
: new DockerWorkloadManager(workloadManagerOptions);
|
||||
|
||||
if (this.isKubernetes) {
|
||||
if (env.POD_CLEANER_ENABLED) {
|
||||
this.logger.log("[ManagedWorker] 🧹 Pod cleaner enabled", {
|
||||
this.logger.log("🧹 Pod cleaner enabled", {
|
||||
namespace: env.KUBERNETES_NAMESPACE,
|
||||
batchSize: env.POD_CLEANER_BATCH_SIZE,
|
||||
intervalMs: env.POD_CLEANER_INTERVAL_MS,
|
||||
@@ -83,11 +95,11 @@ class ManagedSupervisor {
|
||||
intervalMs: env.POD_CLEANER_INTERVAL_MS,
|
||||
});
|
||||
} else {
|
||||
this.logger.warn("[ManagedWorker] Pod cleaner disabled");
|
||||
this.logger.warn("Pod cleaner disabled");
|
||||
}
|
||||
|
||||
if (env.FAILED_POD_HANDLER_ENABLED) {
|
||||
this.logger.log("[ManagedWorker] 🔁 Failed pod handler enabled", {
|
||||
this.logger.log("🔁 Failed pod handler enabled", {
|
||||
namespace: env.KUBERNETES_NAMESPACE,
|
||||
reconnectIntervalMs: env.FAILED_POD_HANDLER_RECONNECT_INTERVAL_MS,
|
||||
});
|
||||
@@ -97,35 +109,41 @@ class ManagedSupervisor {
|
||||
reconnectIntervalMs: env.FAILED_POD_HANDLER_RECONNECT_INTERVAL_MS,
|
||||
});
|
||||
} else {
|
||||
this.logger.warn("[ManagedWorker] Failed pod handler disabled");
|
||||
this.logger.warn("Failed pod handler disabled");
|
||||
}
|
||||
}
|
||||
|
||||
this.resourceMonitor = new KubernetesResourceMonitor(
|
||||
createK8sApi(),
|
||||
env.TRIGGER_WORKER_INSTANCE_NAME
|
||||
if (env.TRIGGER_DEQUEUE_INTERVAL_MS > env.TRIGGER_DEQUEUE_IDLE_INTERVAL_MS) {
|
||||
this.logger.warn(
|
||||
`⚠️ TRIGGER_DEQUEUE_INTERVAL_MS (${env.TRIGGER_DEQUEUE_INTERVAL_MS}) is greater than TRIGGER_DEQUEUE_IDLE_INTERVAL_MS (${env.TRIGGER_DEQUEUE_IDLE_INTERVAL_MS}) - did you mix them up?`
|
||||
);
|
||||
this.workloadManager = new KubernetesWorkloadManager(workloadManagerOptions);
|
||||
} else {
|
||||
this.resourceMonitor = new DockerResourceMonitor(new Docker());
|
||||
this.workloadManager = new DockerWorkloadManager(workloadManagerOptions);
|
||||
}
|
||||
|
||||
this.workerSession = new SupervisorSession({
|
||||
workerToken: env.TRIGGER_WORKER_TOKEN,
|
||||
workerToken: getWorkerToken(),
|
||||
apiUrl: env.TRIGGER_API_URL,
|
||||
instanceName: env.TRIGGER_WORKER_INSTANCE_NAME,
|
||||
managedWorkerSecret: env.MANAGED_WORKER_SECRET,
|
||||
dequeueIntervalMs: env.TRIGGER_DEQUEUE_INTERVAL_MS,
|
||||
dequeueIdleIntervalMs: env.TRIGGER_DEQUEUE_IDLE_INTERVAL_MS,
|
||||
queueConsumerEnabled: env.TRIGGER_DEQUEUE_ENABLED,
|
||||
maxRunCount: env.TRIGGER_DEQUEUE_MAX_RUN_COUNT,
|
||||
maxConsumerCount: env.TRIGGER_DEQUEUE_MAX_CONSUMER_COUNT,
|
||||
runNotificationsEnabled: env.TRIGGER_WORKLOAD_API_ENABLED,
|
||||
heartbeatIntervalSeconds: env.TRIGGER_WORKER_HEARTBEAT_INTERVAL_SECONDS,
|
||||
sendRunDebugLogs: env.SEND_RUN_DEBUG_LOGS,
|
||||
preDequeue: async () => {
|
||||
if (!env.RESOURCE_MONITOR_ENABLED) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (this.isKubernetes) {
|
||||
// Not used in k8s for now
|
||||
return {};
|
||||
}
|
||||
|
||||
const resources = await this.resourceMonitor.getNodeResources();
|
||||
|
||||
return {
|
||||
maxResources: {
|
||||
cpu: resources.cpuAvailable,
|
||||
@@ -141,7 +159,7 @@ class ManagedSupervisor {
|
||||
});
|
||||
|
||||
if (env.TRIGGER_CHECKPOINT_URL) {
|
||||
this.logger.log("[ManagedWorker] 🥶 Checkpoints enabled", {
|
||||
this.logger.log("🥶 Checkpoints enabled", {
|
||||
checkpointUrl: env.TRIGGER_CHECKPOINT_URL,
|
||||
});
|
||||
|
||||
@@ -152,43 +170,34 @@ class ManagedSupervisor {
|
||||
});
|
||||
}
|
||||
|
||||
// setInterval(async () => {
|
||||
// const resources = await this.resourceMonitor.getNodeResources(true);
|
||||
// this.logger.debug("[ManagedWorker] Current resources", { resources });
|
||||
// }, 1000);
|
||||
|
||||
this.workerSession.on("runNotification", async ({ time, run }) => {
|
||||
this.logger.log("[ManagedWorker] runNotification", { time, run });
|
||||
this.logger.log("runNotification", { time, run });
|
||||
|
||||
this.workloadServer.notifyRun({ run });
|
||||
});
|
||||
|
||||
this.workerSession.on("runQueueMessage", async ({ time, message }) => {
|
||||
this.logger.log(
|
||||
`[ManagedWorker] Received message with timestamp ${time.toLocaleString()}`,
|
||||
message
|
||||
);
|
||||
this.logger.log(`Received message with timestamp ${time.toLocaleString()}`, message);
|
||||
|
||||
if (message.completedWaitpoints.length > 0) {
|
||||
this.logger.debug("[ManagedWorker] Run has completed waitpoints", {
|
||||
this.logger.debug("Run has completed waitpoints", {
|
||||
runId: message.run.id,
|
||||
completedWaitpoints: message.completedWaitpoints.length,
|
||||
});
|
||||
// TODO: Do something with them or if we don't need the data here, maybe we shouldn't even send it
|
||||
}
|
||||
|
||||
if (!message.image) {
|
||||
this.logger.error("[ManagedWorker] Run has no image", { runId: message.run.id });
|
||||
this.logger.error("Run has no image", { runId: message.run.id });
|
||||
return;
|
||||
}
|
||||
|
||||
const { checkpoint, ...rest } = message;
|
||||
|
||||
if (checkpoint) {
|
||||
this.logger.log("[ManagedWorker] Restoring run", { runId: message.run.id });
|
||||
this.logger.log("Restoring run", { runId: message.run.id });
|
||||
|
||||
if (!this.checkpointClient) {
|
||||
this.logger.error("[ManagedWorker] No checkpoint client", { runId: message.run.id });
|
||||
this.logger.error("No checkpoint client", { runId: message.run.id });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,23 +212,23 @@ class ManagedSupervisor {
|
||||
});
|
||||
|
||||
if (didRestore) {
|
||||
this.logger.log("[ManagedWorker] Restore successful", { runId: message.run.id });
|
||||
this.logger.log("Restore successful", { runId: message.run.id });
|
||||
} else {
|
||||
this.logger.error("[ManagedWorker] Restore failed", { runId: message.run.id });
|
||||
this.logger.error("Restore failed", { runId: message.run.id });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("[ManagedWorker] Failed to restore run", { error });
|
||||
this.logger.error("Failed to restore run", { error });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("[ManagedWorker] Scheduling run", { runId: message.run.id });
|
||||
this.logger.log("Scheduling run", { runId: message.run.id });
|
||||
|
||||
const didWarmStart = await this.tryWarmStart(message);
|
||||
|
||||
if (didWarmStart) {
|
||||
this.logger.log("[ManagedWorker] Warm start successful", { runId: message.run.id });
|
||||
this.logger.log("Warm start successful", { runId: message.run.id });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,6 +247,7 @@ class ManagedSupervisor {
|
||||
nextAttemptNumber: message.run.attemptNumber,
|
||||
snapshotId: message.snapshot.id,
|
||||
snapshotFriendlyId: message.snapshot.friendlyId,
|
||||
placementTags: message.placementTags,
|
||||
});
|
||||
|
||||
// Disabled for now
|
||||
@@ -246,7 +256,7 @@ class ManagedSupervisor {
|
||||
// memory: message.run.machine.memory,
|
||||
// });
|
||||
} catch (error) {
|
||||
this.logger.error("[ManagedWorker] Failed to create workload", { error });
|
||||
this.logger.error("Failed to create workload", { error });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -274,12 +284,12 @@ class ManagedSupervisor {
|
||||
}
|
||||
|
||||
async onRunConnected({ run }: { run: { friendlyId: string } }) {
|
||||
this.logger.debug("[ManagedWorker] Run connected", { run });
|
||||
this.logger.debug("Run connected", { run });
|
||||
this.workerSession.subscribeToRunNotifications([run.friendlyId]);
|
||||
}
|
||||
|
||||
async onRunDisconnected({ run }: { run: { friendlyId: string } }) {
|
||||
this.logger.debug("[ManagedWorker] Run disconnected", { run });
|
||||
this.logger.debug("Run disconnected", { run });
|
||||
this.workerSession.unsubscribeFromRunNotifications([run.friendlyId]);
|
||||
}
|
||||
|
||||
@@ -300,7 +310,7 @@ class ManagedSupervisor {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
this.logger.error("[ManagedWorker] Warm start failed", {
|
||||
this.logger.error("Warm start failed", {
|
||||
runId: dequeuedMessage.run.id,
|
||||
});
|
||||
return false;
|
||||
@@ -310,7 +320,7 @@ class ManagedSupervisor {
|
||||
const parsedData = z.object({ didWarmStart: z.boolean() }).safeParse(data);
|
||||
|
||||
if (!parsedData.success) {
|
||||
this.logger.error("[ManagedWorker] Warm start response invalid", {
|
||||
this.logger.error("Warm start response invalid", {
|
||||
runId: dequeuedMessage.run.id,
|
||||
data,
|
||||
});
|
||||
@@ -319,7 +329,7 @@ class ManagedSupervisor {
|
||||
|
||||
return parsedData.data.didWarmStart;
|
||||
} catch (error) {
|
||||
this.logger.error("[ManagedWorker] Warm start error", {
|
||||
this.logger.error("Warm start error", {
|
||||
runId: dequeuedMessage.run.id,
|
||||
error,
|
||||
});
|
||||
@@ -328,7 +338,7 @@ class ManagedSupervisor {
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.logger.log("[ManagedWorker] Starting up");
|
||||
this.logger.log("Starting up");
|
||||
|
||||
// Optional services
|
||||
await this.podCleaner?.start();
|
||||
@@ -336,21 +346,21 @@ class ManagedSupervisor {
|
||||
await this.metricsServer?.start();
|
||||
|
||||
if (env.TRIGGER_WORKLOAD_API_ENABLED) {
|
||||
this.logger.log("[ManagedWorker] Workload API enabled", {
|
||||
this.logger.log("Workload API enabled", {
|
||||
protocol: env.TRIGGER_WORKLOAD_API_PROTOCOL,
|
||||
domain: env.TRIGGER_WORKLOAD_API_DOMAIN,
|
||||
port: env.TRIGGER_WORKLOAD_API_PORT_INTERNAL,
|
||||
});
|
||||
await this.workloadServer.start();
|
||||
} else {
|
||||
this.logger.warn("[ManagedWorker] Workload API disabled");
|
||||
this.logger.warn("Workload API disabled");
|
||||
}
|
||||
|
||||
await this.workerSession.start();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.logger.log("[ManagedWorker] Shutting down");
|
||||
this.logger.log("Shutting down");
|
||||
await this.workerSession.stop();
|
||||
|
||||
// Optional services
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type Docker from "dockerode";
|
||||
import type * as TDocker from "docker-api-ts";
|
||||
import type { MachineResources } from "@trigger.dev/core/v3";
|
||||
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
import { env } from "./env.js";
|
||||
@@ -71,18 +70,21 @@ export abstract class ResourceMonitor {
|
||||
}
|
||||
|
||||
protected applyOverrides(resources: NodeResources): NodeResources {
|
||||
if (!env.OVERRIDE_CPU_TOTAL && !env.OVERRIDE_MEMORY_TOTAL_GB) {
|
||||
if (
|
||||
!env.RESOURCE_MONITOR_OVERRIDE_CPU_TOTAL &&
|
||||
!env.RESOURCE_MONITOR_OVERRIDE_MEMORY_TOTAL_GB
|
||||
) {
|
||||
return resources;
|
||||
}
|
||||
|
||||
logger.debug("[ResourceMonitor] 🛡️ Applying resource overrides", {
|
||||
cpuTotal: env.OVERRIDE_CPU_TOTAL,
|
||||
memoryTotalGb: env.OVERRIDE_MEMORY_TOTAL_GB,
|
||||
cpuTotal: env.RESOURCE_MONITOR_OVERRIDE_CPU_TOTAL,
|
||||
memoryTotalGb: env.RESOURCE_MONITOR_OVERRIDE_MEMORY_TOTAL_GB,
|
||||
});
|
||||
|
||||
const cpuTotal = env.OVERRIDE_CPU_TOTAL ?? resources.cpuTotal;
|
||||
const memoryTotal = env.OVERRIDE_MEMORY_TOTAL_GB
|
||||
? this.gbToBytes(env.OVERRIDE_MEMORY_TOTAL_GB)
|
||||
const cpuTotal = env.RESOURCE_MONITOR_OVERRIDE_CPU_TOTAL ?? resources.cpuTotal;
|
||||
const memoryTotal = env.RESOURCE_MONITOR_OVERRIDE_MEMORY_TOTAL_GB
|
||||
? this.gbToBytes(env.RESOURCE_MONITOR_OVERRIDE_MEMORY_TOTAL_GB)
|
||||
: resources.memoryTotal;
|
||||
|
||||
const cpuDiff = cpuTotal - resources.cpuTotal;
|
||||
@@ -100,6 +102,11 @@ export abstract class ResourceMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
type SystemInfo = {
|
||||
NCPU: number | undefined;
|
||||
MemTotal: number | undefined;
|
||||
};
|
||||
|
||||
export class DockerResourceMonitor extends ResourceMonitor {
|
||||
private docker: Docker;
|
||||
|
||||
@@ -114,7 +121,7 @@ export class DockerResourceMonitor extends ResourceMonitor {
|
||||
return this.cachedResources;
|
||||
}
|
||||
|
||||
const info: TDocker.SystemInfo = await this.docker.info();
|
||||
const info: SystemInfo = await this.docker.info();
|
||||
const stats = await this.docker.listContainers({ all: true });
|
||||
|
||||
// Get system-wide resources
|
||||
@@ -208,6 +215,21 @@ export class KubernetesResourceMonitor extends ResourceMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
export class NoopResourceMonitor extends ResourceMonitor {
|
||||
constructor() {
|
||||
super(NoopResourceParser);
|
||||
}
|
||||
|
||||
async getNodeResources(): Promise<NodeResources> {
|
||||
return {
|
||||
cpuTotal: 0,
|
||||
cpuAvailable: Infinity,
|
||||
memoryTotal: 0,
|
||||
memoryAvailable: Infinity,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ResourceParser {
|
||||
abstract cpu(cpu: number | string): number;
|
||||
abstract memory(memory: number | string): number;
|
||||
@@ -244,3 +266,13 @@ class KubernetesResourceParser extends ResourceParser {
|
||||
return parseInt(memory);
|
||||
}
|
||||
}
|
||||
|
||||
class NoopResourceParser extends ResourceParser {
|
||||
cpu(cpu: number): number {
|
||||
return cpu;
|
||||
}
|
||||
|
||||
memory(memory: number): number {
|
||||
return memory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
export function getDockerHostDomain() {
|
||||
const isMacOs = process.platform === "darwin";
|
||||
const isWindows = process.platform === "win32";
|
||||
import { isMacOS, isWindows } from "std-env";
|
||||
|
||||
return isMacOs || isWindows ? "host.docker.internal" : "localhost";
|
||||
export function normalizeDockerHostUrl(url: string) {
|
||||
const $url = new URL(url);
|
||||
|
||||
if ($url.hostname === "localhost") {
|
||||
$url.hostname = getDockerHostDomain();
|
||||
}
|
||||
|
||||
return $url.toString();
|
||||
}
|
||||
|
||||
export function getDockerHostDomain() {
|
||||
return isMacOS || isWindows ? "host.docker.internal" : "localhost";
|
||||
}
|
||||
|
||||
export function getRunnerId(runId: string, attemptNumber?: number) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { env } from "./env.js";
|
||||
|
||||
export function getWorkerToken() {
|
||||
if (!env.TRIGGER_WORKER_TOKEN.startsWith("file://")) {
|
||||
return env.TRIGGER_WORKER_TOKEN;
|
||||
}
|
||||
|
||||
const tokenPath = env.TRIGGER_WORKER_TOKEN.replace("file://", "");
|
||||
|
||||
console.debug(
|
||||
JSON.stringify({
|
||||
message: "🔑 Reading worker token from file",
|
||||
tokenPath,
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const token = readFileSync(tokenPath, "utf8").trim();
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error(`Failed to read worker token from file: ${tokenPath}`, error);
|
||||
throw new Error(
|
||||
`Unable to read worker token from file: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,88 +4,269 @@ import {
|
||||
type WorkloadManagerCreateOptions,
|
||||
type WorkloadManagerOptions,
|
||||
} from "./types.js";
|
||||
import { x } from "tinyexec";
|
||||
import { env } from "../env.js";
|
||||
import { getDockerHostDomain, getRunnerId } from "../util.js";
|
||||
import { getDockerHostDomain, getRunnerId, normalizeDockerHostUrl } from "../util.js";
|
||||
import Docker from "dockerode";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
|
||||
export class DockerWorkloadManager implements WorkloadManager {
|
||||
private readonly logger = new SimpleStructuredLogger("docker-workload-provider");
|
||||
private readonly logger = new SimpleStructuredLogger("docker-workload-manager");
|
||||
private readonly docker: Docker;
|
||||
|
||||
private readonly runnerNetworks: string[];
|
||||
private readonly auth?: Docker.AuthConfig;
|
||||
private readonly platformOverride?: string;
|
||||
|
||||
constructor(private opts: WorkloadManagerOptions) {
|
||||
this.docker = new Docker({
|
||||
version: env.DOCKER_API_VERSION,
|
||||
});
|
||||
|
||||
if (opts.workloadApiDomain) {
|
||||
this.logger.warn("[DockerWorkloadProvider] ⚠️ Custom workload API domain", {
|
||||
this.logger.warn("⚠️ Custom workload API domain", {
|
||||
domain: opts.workloadApiDomain,
|
||||
});
|
||||
}
|
||||
|
||||
this.runnerNetworks = env.DOCKER_RUNNER_NETWORKS.split(",");
|
||||
|
||||
this.platformOverride = env.DOCKER_PLATFORM;
|
||||
if (this.platformOverride) {
|
||||
this.logger.info("🖥️ Platform override", {
|
||||
targetPlatform: this.platformOverride,
|
||||
hostPlatform: process.arch,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.DOCKER_REGISTRY_USERNAME && env.DOCKER_REGISTRY_PASSWORD && env.DOCKER_REGISTRY_URL) {
|
||||
this.logger.info("🐋 Using Docker registry credentials", {
|
||||
username: env.DOCKER_REGISTRY_USERNAME,
|
||||
url: env.DOCKER_REGISTRY_URL,
|
||||
});
|
||||
|
||||
this.auth = {
|
||||
username: env.DOCKER_REGISTRY_USERNAME,
|
||||
password: env.DOCKER_REGISTRY_PASSWORD,
|
||||
serveraddress: env.DOCKER_REGISTRY_URL,
|
||||
};
|
||||
} else {
|
||||
this.logger.warn("🐋 No Docker registry credentials provided, skipping auth");
|
||||
}
|
||||
}
|
||||
|
||||
async create(opts: WorkloadManagerCreateOptions) {
|
||||
this.logger.log("[DockerWorkloadProvider] Creating container", { opts });
|
||||
this.logger.log("create()", { opts });
|
||||
|
||||
const runnerId = getRunnerId(opts.runFriendlyId, opts.nextAttemptNumber);
|
||||
|
||||
const runArgs = [
|
||||
"run",
|
||||
"--detach",
|
||||
`--network=${env.DOCKER_NETWORK}`,
|
||||
`--env=TRIGGER_DEQUEUED_AT_MS=${opts.dequeuedAt.getTime()}`,
|
||||
`--env=TRIGGER_POD_SCHEDULED_AT_MS=${Date.now()}`,
|
||||
`--env=TRIGGER_ENV_ID=${opts.envId}`,
|
||||
`--env=TRIGGER_RUN_ID=${opts.runFriendlyId}`,
|
||||
`--env=TRIGGER_SNAPSHOT_ID=${opts.snapshotFriendlyId}`,
|
||||
`--env=TRIGGER_SUPERVISOR_API_PROTOCOL=${this.opts.workloadApiProtocol}`,
|
||||
`--env=TRIGGER_SUPERVISOR_API_PORT=${this.opts.workloadApiPort}`,
|
||||
`--env=TRIGGER_SUPERVISOR_API_DOMAIN=${this.opts.workloadApiDomain ?? getDockerHostDomain()}`,
|
||||
`--env=TRIGGER_WORKER_INSTANCE_NAME=${env.TRIGGER_WORKER_INSTANCE_NAME}`,
|
||||
`--env=OTEL_EXPORTER_OTLP_ENDPOINT=${env.OTEL_EXPORTER_OTLP_ENDPOINT}`,
|
||||
`--env=TRIGGER_RUNNER_ID=${runnerId}`,
|
||||
`--hostname=${runnerId}`,
|
||||
`--name=${runnerId}`,
|
||||
// Build environment variables
|
||||
const envVars: string[] = [
|
||||
`OTEL_EXPORTER_OTLP_ENDPOINT=${env.OTEL_EXPORTER_OTLP_ENDPOINT}`,
|
||||
`TRIGGER_DEQUEUED_AT_MS=${opts.dequeuedAt.getTime()}`,
|
||||
`TRIGGER_POD_SCHEDULED_AT_MS=${Date.now()}`,
|
||||
`TRIGGER_ENV_ID=${opts.envId}`,
|
||||
`TRIGGER_RUN_ID=${opts.runFriendlyId}`,
|
||||
`TRIGGER_SNAPSHOT_ID=${opts.snapshotFriendlyId}`,
|
||||
`TRIGGER_SUPERVISOR_API_PROTOCOL=${this.opts.workloadApiProtocol}`,
|
||||
`TRIGGER_SUPERVISOR_API_PORT=${this.opts.workloadApiPort}`,
|
||||
`TRIGGER_SUPERVISOR_API_DOMAIN=${this.opts.workloadApiDomain ?? getDockerHostDomain()}`,
|
||||
`TRIGGER_WORKER_INSTANCE_NAME=${env.TRIGGER_WORKER_INSTANCE_NAME}`,
|
||||
`TRIGGER_RUNNER_ID=${runnerId}`,
|
||||
`TRIGGER_MACHINE_CPU=${opts.machine.cpu}`,
|
||||
`TRIGGER_MACHINE_MEMORY=${opts.machine.memory}`,
|
||||
`PRETTY_LOGS=${env.RUNNER_PRETTY_LOGS}`,
|
||||
];
|
||||
|
||||
if (this.opts.dockerAutoremove) {
|
||||
runArgs.push("--rm");
|
||||
}
|
||||
|
||||
if (this.opts.warmStartUrl) {
|
||||
runArgs.push(`--env=TRIGGER_WARM_START_URL=${this.opts.warmStartUrl}`);
|
||||
envVars.push(`TRIGGER_WARM_START_URL=${normalizeDockerHostUrl(this.opts.warmStartUrl)}`);
|
||||
}
|
||||
|
||||
if (this.opts.metadataUrl) {
|
||||
runArgs.push(`--env=TRIGGER_METADATA_URL=${this.opts.metadataUrl}`);
|
||||
envVars.push(`TRIGGER_METADATA_URL=${this.opts.metadataUrl}`);
|
||||
}
|
||||
|
||||
if (this.opts.heartbeatIntervalSeconds) {
|
||||
runArgs.push(
|
||||
`--env=TRIGGER_HEARTBEAT_INTERVAL_SECONDS=${this.opts.heartbeatIntervalSeconds}`
|
||||
);
|
||||
envVars.push(`TRIGGER_HEARTBEAT_INTERVAL_SECONDS=${this.opts.heartbeatIntervalSeconds}`);
|
||||
}
|
||||
|
||||
if (this.opts.snapshotPollIntervalSeconds) {
|
||||
runArgs.push(
|
||||
`--env=TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS=${this.opts.snapshotPollIntervalSeconds}`
|
||||
envVars.push(
|
||||
`TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS=${this.opts.snapshotPollIntervalSeconds}`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.opts.additionalEnvVars) {
|
||||
Object.entries(this.opts.additionalEnvVars).forEach(([key, value]) => {
|
||||
runArgs.push(`--env=${key}=${value}`);
|
||||
envVars.push(`${key}=${value}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (env.ENFORCE_MACHINE_PRESETS) {
|
||||
runArgs.push(`--cpus=${opts.machine.cpu}`, `--memory=${opts.machine.memory}G`);
|
||||
runArgs.push(`--env=TRIGGER_MACHINE_CPU=${opts.machine.cpu}`);
|
||||
runArgs.push(`--env=TRIGGER_MACHINE_MEMORY=${opts.machine.memory}`);
|
||||
const hostConfig: Docker.HostConfig = {
|
||||
AutoRemove: !!this.opts.dockerAutoremove,
|
||||
};
|
||||
|
||||
const [firstNetwork, ...remainingNetworks] = this.runnerNetworks;
|
||||
|
||||
// Always attach the first network at container creation time. This has the following benefits:
|
||||
// - If there is only a single network to attach, this will prevent having to make a separate request.
|
||||
// - If there are multiple networks to attach, this will ensure the runner won't also be connected to the bridge network
|
||||
hostConfig.NetworkMode = firstNetwork;
|
||||
|
||||
if (env.DOCKER_ENFORCE_MACHINE_PRESETS) {
|
||||
hostConfig.NanoCpus = opts.machine.cpu * 1e9;
|
||||
hostConfig.Memory = opts.machine.memory * 1024 * 1024 * 1024;
|
||||
}
|
||||
|
||||
runArgs.push(`${opts.image}`);
|
||||
let imageRef = opts.image;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await x("docker", runArgs);
|
||||
this.logger.debug("[DockerWorkloadProvider] Create succeeded", { stdout, stderr });
|
||||
} catch (error) {
|
||||
this.logger.error("[DockerWorkloadProvider] Create failed:", { opts, error });
|
||||
if (env.DOCKER_STRIP_IMAGE_DIGEST) {
|
||||
imageRef = opts.image.split("@")[0]!;
|
||||
}
|
||||
|
||||
const containerCreateOpts: Docker.ContainerCreateOptions = {
|
||||
name: runnerId,
|
||||
Hostname: runnerId,
|
||||
HostConfig: hostConfig,
|
||||
Image: imageRef,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
AttachStdin: false,
|
||||
};
|
||||
|
||||
if (this.platformOverride) {
|
||||
containerCreateOpts.platform = this.platformOverride;
|
||||
}
|
||||
|
||||
const logger = this.logger.child({ opts, containerCreateOpts });
|
||||
|
||||
const [inspectError, inspectResult] = await tryCatch(this.docker.getImage(imageRef).inspect());
|
||||
|
||||
let shouldPull = !!inspectError;
|
||||
if (this.platformOverride) {
|
||||
const imageArchitecture = inspectResult?.Architecture;
|
||||
|
||||
// When the image architecture doesn't match the platform, we need to pull the image
|
||||
if (imageArchitecture && !this.platformOverride.includes(imageArchitecture)) {
|
||||
shouldPull = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the image is not present, try to pull it
|
||||
if (shouldPull) {
|
||||
logger.info("Pulling image", {
|
||||
error: inspectError,
|
||||
image: opts.image,
|
||||
targetPlatform: this.platformOverride,
|
||||
imageArchitecture: inspectResult?.Architecture,
|
||||
});
|
||||
|
||||
// Ensure the image is present
|
||||
const [createImageError, imageResponseReader] = await tryCatch(
|
||||
this.docker.createImage(this.auth, {
|
||||
fromImage: imageRef,
|
||||
...(this.platformOverride ? { platform: this.platformOverride } : {}),
|
||||
})
|
||||
);
|
||||
if (createImageError) {
|
||||
logger.error("Failed to pull image", { error: createImageError });
|
||||
return;
|
||||
}
|
||||
|
||||
const [imageReadError, imageResponse] = await tryCatch(readAllChunks(imageResponseReader));
|
||||
if (imageReadError) {
|
||||
logger.error("failed to read image response", { error: imageReadError });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("pulled image", { image: opts.image, imageResponse });
|
||||
} else {
|
||||
// Image is present, so we can use it to create the container
|
||||
}
|
||||
|
||||
// Create container
|
||||
const [createContainerError, container] = await tryCatch(
|
||||
this.docker.createContainer({
|
||||
...containerCreateOpts,
|
||||
// Add env vars here so they're not logged
|
||||
Env: envVars,
|
||||
})
|
||||
);
|
||||
|
||||
if (createContainerError) {
|
||||
logger.error("Failed to create container", { error: createContainerError });
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are multiple networks to attach to we need to attach the remaining ones after creation
|
||||
if (remainingNetworks.length > 0) {
|
||||
await this.attachContainerToNetworks({
|
||||
containerId: container.id,
|
||||
networkNames: remainingNetworks,
|
||||
});
|
||||
}
|
||||
|
||||
// Start container
|
||||
const [startError, startResult] = await tryCatch(container.start());
|
||||
|
||||
if (startError) {
|
||||
logger.error("Failed to start container", { error: startError, containerId: container.id });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("create succeeded", { startResult, containerId: container.id });
|
||||
}
|
||||
|
||||
private async attachContainerToNetworks({
|
||||
containerId,
|
||||
networkNames,
|
||||
}: {
|
||||
containerId: string;
|
||||
networkNames: string[];
|
||||
}) {
|
||||
this.logger.debug("Attaching container to networks", { containerId, networkNames });
|
||||
|
||||
const [error, networkResults] = await tryCatch(
|
||||
this.docker.listNetworks({
|
||||
filters: {
|
||||
// Full name matches only to prevent unexpected results
|
||||
name: networkNames.map((name) => `^${name}$`),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (error) {
|
||||
this.logger.error("Failed to list networks", { networkNames });
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
networkResults.map((networkInfo) => {
|
||||
const network = this.docker.getNetwork(networkInfo.Id);
|
||||
return network.connect({ Container: containerId });
|
||||
})
|
||||
);
|
||||
|
||||
if (results.some((r) => r.status === "rejected")) {
|
||||
this.logger.error("Failed to attach container to some networks", {
|
||||
containerId,
|
||||
networkNames,
|
||||
results,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug("Attached container to networks", {
|
||||
containerId,
|
||||
networkNames,
|
||||
results,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function readAllChunks(reader: NodeJS.ReadableStream) {
|
||||
const chunks = [];
|
||||
for await (const chunk of reader) {
|
||||
chunks.push(chunk.toString());
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
type WorkloadManagerCreateOptions,
|
||||
type WorkloadManagerOptions,
|
||||
} from "./types.js";
|
||||
import type { EnvironmentType, MachinePreset } from "@trigger.dev/core/v3";
|
||||
import type { EnvironmentType, MachinePreset, PlacementTag } from "@trigger.dev/core/v3";
|
||||
import { PlacementTagProcessor } from "@trigger.dev/core/v3/serverOnly";
|
||||
import { env } from "../env.js";
|
||||
import { type K8sApi, createK8sApi, type k8s } from "../clients/kubernetes.js";
|
||||
import { getRunnerId } from "../util.js";
|
||||
@@ -17,9 +18,14 @@ export class KubernetesWorkloadManager implements WorkloadManager {
|
||||
private readonly logger = new SimpleStructuredLogger("kubernetes-workload-provider");
|
||||
private k8s: K8sApi;
|
||||
private namespace = env.KUBERNETES_NAMESPACE;
|
||||
private placementTagProcessor: PlacementTagProcessor;
|
||||
|
||||
constructor(private opts: WorkloadManagerOptions) {
|
||||
this.k8s = createK8sApi();
|
||||
this.placementTagProcessor = new PlacementTagProcessor({
|
||||
enabled: env.PLACEMENT_TAGS_ENABLED,
|
||||
prefix: env.PLACEMENT_TAGS_PREFIX,
|
||||
});
|
||||
|
||||
if (opts.workloadApiDomain) {
|
||||
this.logger.warn("[KubernetesWorkloadManager] ⚠️ Custom workload API domain", {
|
||||
@@ -28,6 +34,21 @@ export class KubernetesWorkloadManager implements WorkloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
private addPlacementTags(
|
||||
podSpec: Omit<k8s.V1PodSpec, "containers">,
|
||||
placementTags?: PlacementTag[]
|
||||
): Omit<k8s.V1PodSpec, "containers"> {
|
||||
const nodeSelector = this.placementTagProcessor.convertToNodeSelector(
|
||||
placementTags,
|
||||
podSpec.nodeSelector
|
||||
);
|
||||
|
||||
return {
|
||||
...podSpec,
|
||||
nodeSelector,
|
||||
};
|
||||
}
|
||||
|
||||
async create(opts: WorkloadManagerCreateOptions) {
|
||||
this.logger.log("[KubernetesWorkloadManager] Creating container", { opts });
|
||||
|
||||
@@ -48,7 +69,7 @@ export class KubernetesWorkloadManager implements WorkloadManager {
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
...this.#defaultPodSpec,
|
||||
...this.addPlacementTags(this.#defaultPodSpec, opts.placementTags),
|
||||
terminationGracePeriodSeconds: 60 * 60,
|
||||
containers: [
|
||||
{
|
||||
@@ -228,21 +249,25 @@ export class KubernetesWorkloadManager implements WorkloadManager {
|
||||
restartPolicy: "Never",
|
||||
automountServiceAccountToken: false,
|
||||
imagePullSecrets: this.getImagePullSecrets(),
|
||||
nodeSelector: {
|
||||
nodetype: env.KUBERNETES_WORKER_NODETYPE_LABEL,
|
||||
},
|
||||
...(env.KUBERNETES_WORKER_NODETYPE_LABEL
|
||||
? {
|
||||
nodeSelector: {
|
||||
nodetype: env.KUBERNETES_WORKER_NODETYPE_LABEL,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
get #defaultResourceRequests(): ResourceQuantities {
|
||||
return {
|
||||
"ephemeral-storage": env.EPHEMERAL_STORAGE_SIZE_REQUEST,
|
||||
"ephemeral-storage": env.KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
get #defaultResourceLimits(): ResourceQuantities {
|
||||
return {
|
||||
"ephemeral-storage": env.EPHEMERAL_STORAGE_SIZE_LIMIT,
|
||||
"ephemeral-storage": env.KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type EnvironmentType, type MachinePreset } from "@trigger.dev/core/v3";
|
||||
import type { EnvironmentType, MachinePreset, PlacementTag } from "@trigger.dev/core/v3";
|
||||
|
||||
export interface WorkloadManagerOptions {
|
||||
workloadApiProtocol: "http" | "https";
|
||||
@@ -23,6 +23,7 @@ export interface WorkloadManagerCreateOptions {
|
||||
version: string;
|
||||
nextAttemptNumber?: number;
|
||||
dequeuedAt: Date;
|
||||
placementTags?: PlacementTag[];
|
||||
// identifiers
|
||||
envId: string;
|
||||
envType: EnvironmentType;
|
||||
|
||||
@@ -17,12 +17,14 @@ import {
|
||||
WorkloadRunAttemptStartRequestBody,
|
||||
type WorkloadRunAttemptStartResponseBody,
|
||||
type WorkloadRunLatestSnapshotResponseBody,
|
||||
WorkloadRunSnapshotsSinceResponseBody,
|
||||
type WorkloadServerToClientEvents,
|
||||
type WorkloadSuspendRunResponseBody,
|
||||
} from "@trigger.dev/core/v3/workers";
|
||||
import { HttpServer, type CheckpointClient } from "@trigger.dev/core/v3/serverOnly";
|
||||
import { type IncomingMessage } from "node:http";
|
||||
import { register } from "../metrics.js";
|
||||
import { env } from "../env.js";
|
||||
|
||||
// Use the official export when upgrading to socket.io@4.8.0
|
||||
interface DefaultEventsMap {
|
||||
@@ -62,6 +64,8 @@ type WorkloadServerOptions = {
|
||||
export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
private checkpointClient?: CheckpointClient;
|
||||
|
||||
private readonly logger = new SimpleStructuredLogger("workload-server");
|
||||
|
||||
private readonly httpServer: HttpServer;
|
||||
private readonly websocketServer: Namespace<
|
||||
WorkloadClientToServerEvents,
|
||||
@@ -150,7 +154,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
);
|
||||
|
||||
if (!startResponse.success) {
|
||||
console.error("Failed to start run", {
|
||||
this.logger.error("Failed to start run", {
|
||||
params,
|
||||
error: startResponse.error,
|
||||
});
|
||||
@@ -170,7 +174,6 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
paramsSchema: WorkloadActionParams,
|
||||
bodySchema: WorkloadRunAttemptCompleteRequestBody,
|
||||
handler: async ({ req, reply, params, body }) => {
|
||||
console.log("headers", req.headers);
|
||||
const completeResponse = await this.workerClient.completeRunAttempt(
|
||||
params.runFriendlyId,
|
||||
params.snapshotFriendlyId,
|
||||
@@ -179,7 +182,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
);
|
||||
|
||||
if (!completeResponse.success) {
|
||||
console.error("Failed to complete run", {
|
||||
this.logger.error("Failed to complete run", {
|
||||
params,
|
||||
error: completeResponse.error,
|
||||
});
|
||||
@@ -207,7 +210,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
);
|
||||
|
||||
if (!heartbeatResponse.success) {
|
||||
console.error("Failed to heartbeat run", {
|
||||
this.logger.error("Failed to heartbeat run", {
|
||||
params,
|
||||
error: heartbeatResponse.error,
|
||||
});
|
||||
@@ -227,7 +230,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
{
|
||||
paramsSchema: WorkloadActionParams,
|
||||
handler: async ({ reply, params, req }) => {
|
||||
console.debug("Suspend request", { params, headers: req.headers });
|
||||
this.logger.debug("Suspend request", { params, headers: req.headers });
|
||||
|
||||
if (!this.checkpointClient) {
|
||||
reply.json(
|
||||
@@ -246,7 +249,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
const projectRef = this.projectRefFromRequest(req);
|
||||
|
||||
if (!runnerId || !deploymentVersion || !projectRef) {
|
||||
console.error("Invalid headers for suspend request", {
|
||||
this.logger.error("Invalid headers for suspend request", {
|
||||
...params,
|
||||
headers: req.headers,
|
||||
});
|
||||
@@ -282,7 +285,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
});
|
||||
|
||||
if (!suspendResult) {
|
||||
console.error("Failed to suspend run", { params });
|
||||
this.logger.error("Failed to suspend run", { params });
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -294,7 +297,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
{
|
||||
paramsSchema: WorkloadActionParams,
|
||||
handler: async ({ req, reply, params }) => {
|
||||
console.debug("Run continuation request", { params });
|
||||
this.logger.debug("Run continuation request", { params });
|
||||
|
||||
const continuationResult = await this.workerClient.continueRunExecution(
|
||||
params.runFriendlyId,
|
||||
@@ -303,7 +306,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
);
|
||||
|
||||
if (!continuationResult.success) {
|
||||
console.error("Failed to continue run execution", { params });
|
||||
this.logger.error("Failed to continue run execution", { params });
|
||||
reply.json(
|
||||
{
|
||||
ok: false,
|
||||
@@ -328,7 +331,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
);
|
||||
|
||||
if (!latestSnapshotResponse.success) {
|
||||
console.error("Failed to get latest snapshot", {
|
||||
this.logger.error("Failed to get latest snapshot", {
|
||||
runId: params.runFriendlyId,
|
||||
error: latestSnapshotResponse.error,
|
||||
});
|
||||
@@ -341,12 +344,41 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
} satisfies WorkloadRunLatestSnapshotResponseBody);
|
||||
},
|
||||
})
|
||||
.route(
|
||||
"/api/v1/workload-actions/runs/:runFriendlyId/snapshots/since/:snapshotFriendlyId",
|
||||
"GET",
|
||||
{
|
||||
paramsSchema: WorkloadActionParams,
|
||||
handler: async ({ req, reply, params }) => {
|
||||
const sinceSnapshotResponse = await this.workerClient.getSnapshotsSince(
|
||||
params.runFriendlyId,
|
||||
params.snapshotFriendlyId,
|
||||
this.runnerIdFromRequest(req)
|
||||
);
|
||||
|
||||
if (!sinceSnapshotResponse.success) {
|
||||
this.logger.error("Failed to get snapshots since", {
|
||||
runId: params.runFriendlyId,
|
||||
error: sinceSnapshotResponse.error,
|
||||
});
|
||||
reply.empty(500);
|
||||
return;
|
||||
}
|
||||
|
||||
reply.json(sinceSnapshotResponse.data satisfies WorkloadRunSnapshotsSinceResponseBody);
|
||||
},
|
||||
}
|
||||
)
|
||||
.route("/api/v1/workload-actions/runs/:runFriendlyId/logs/debug", "POST", {
|
||||
paramsSchema: WorkloadActionParams.pick({ runFriendlyId: true }),
|
||||
bodySchema: WorkloadDebugLogRequestBody,
|
||||
handler: async ({ req, reply, params, body }) => {
|
||||
reply.empty(204);
|
||||
|
||||
if (!env.SEND_RUN_DEBUG_LOGS) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workerClient.sendDebugLog(
|
||||
params.runFriendlyId,
|
||||
body,
|
||||
@@ -367,7 +399,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
);
|
||||
|
||||
if (!dequeueResponse.success) {
|
||||
console.error("Failed to get latest snapshot", {
|
||||
this.logger.error("Failed to get latest snapshot", {
|
||||
deploymentId: params.deploymentId,
|
||||
error: dequeueResponse.error,
|
||||
});
|
||||
@@ -391,14 +423,14 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
> = io.of("/workload");
|
||||
|
||||
websocketServer.on("disconnect", (socket) => {
|
||||
console.log("[WorkloadSocket] disconnect", socket.id);
|
||||
this.logger.log("[WS] disconnect", socket.id);
|
||||
});
|
||||
websocketServer.use(async (socket, next) => {
|
||||
function setSocketDataFromHeader(
|
||||
const setSocketDataFromHeader = (
|
||||
dataKey: keyof typeof socket.data,
|
||||
headerName: string,
|
||||
required: boolean = true
|
||||
) {
|
||||
) => {
|
||||
const value = socket.handshake.headers[headerName];
|
||||
|
||||
if (value) {
|
||||
@@ -414,27 +446,26 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
}
|
||||
|
||||
if (required) {
|
||||
console.error("[WorkloadSocket] missing required header", { headerName });
|
||||
this.logger.error("[WS] missing required header", { headerName });
|
||||
throw new Error("missing header");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
setSocketDataFromHeader("deploymentId", WORKLOAD_HEADERS.DEPLOYMENT_ID);
|
||||
setSocketDataFromHeader("runnerId", WORKLOAD_HEADERS.RUNNER_ID);
|
||||
} catch (error) {
|
||||
console.error("[WorkloadSocket] setSocketDataFromHeader error", { error });
|
||||
this.logger.error("[WS] setSocketDataFromHeader error", { error });
|
||||
socket.disconnect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("[WorkloadSocket] auth success", socket.data);
|
||||
this.logger.debug("[WS] auth success", socket.data);
|
||||
|
||||
next();
|
||||
});
|
||||
websocketServer.on("connection", (socket) => {
|
||||
const logger = new SimpleStructuredLogger("workload-namespace", undefined, {
|
||||
namespace: "workload",
|
||||
const socketLogger = this.logger.child({
|
||||
socketId: socket.id,
|
||||
socketData: socket.data,
|
||||
});
|
||||
@@ -449,11 +480,11 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
};
|
||||
|
||||
const runConnected = (friendlyId: string) => {
|
||||
logger.debug("runConnected", { ...getSocketMetadata() });
|
||||
socketLogger.debug("runConnected", { ...getSocketMetadata() });
|
||||
|
||||
// If there's already a run ID set, we should "disconnect" it from this socket
|
||||
if (socket.data.runFriendlyId && socket.data.runFriendlyId !== friendlyId) {
|
||||
logger.debug("runConnected: disconnecting existing run", {
|
||||
socketLogger.debug("runConnected: disconnecting existing run", {
|
||||
...getSocketMetadata(),
|
||||
newRunId: friendlyId,
|
||||
oldRunId: socket.data.runFriendlyId,
|
||||
@@ -467,14 +498,14 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
};
|
||||
|
||||
const runDisconnected = (friendlyId: string) => {
|
||||
logger.debug("runDisconnected", { ...getSocketMetadata() });
|
||||
socketLogger.debug("runDisconnected", { ...getSocketMetadata() });
|
||||
|
||||
this.runSockets.delete(friendlyId);
|
||||
this.emit("runDisconnected", { run: { friendlyId } });
|
||||
socket.data.runFriendlyId = undefined;
|
||||
};
|
||||
|
||||
logger.log("wsServer socket connected", { ...getSocketMetadata() });
|
||||
socketLogger.log("wsServer socket connected", { ...getSocketMetadata() });
|
||||
|
||||
// FIXME: where does this get set?
|
||||
if (socket.data.runFriendlyId) {
|
||||
@@ -482,7 +513,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
}
|
||||
|
||||
socket.on("disconnecting", (reason, description) => {
|
||||
logger.log("Socket disconnecting", { ...getSocketMetadata(), reason, description });
|
||||
socketLogger.log("Socket disconnecting", { ...getSocketMetadata(), reason, description });
|
||||
|
||||
if (socket.data.runFriendlyId) {
|
||||
runDisconnected(socket.data.runFriendlyId);
|
||||
@@ -490,11 +521,11 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason, description) => {
|
||||
logger.log("Socket disconnected", { ...getSocketMetadata(), reason, description });
|
||||
socketLogger.log("Socket disconnected", { ...getSocketMetadata(), reason, description });
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
logger.error("Socket error", {
|
||||
socketLogger.error("Socket error", {
|
||||
...getSocketMetadata(),
|
||||
error: {
|
||||
name: error.name,
|
||||
@@ -505,7 +536,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
});
|
||||
|
||||
socket.on("run:start", async (message) => {
|
||||
const log = logger.child({
|
||||
const log = socketLogger.child({
|
||||
eventName: "run:start",
|
||||
...getSocketMetadata(),
|
||||
...message,
|
||||
@@ -521,7 +552,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
});
|
||||
|
||||
socket.on("run:stop", async (message) => {
|
||||
const log = logger.child({
|
||||
const log = socketLogger.child({
|
||||
eventName: "run:stop",
|
||||
...getSocketMetadata(),
|
||||
...message,
|
||||
@@ -545,7 +576,7 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
const runSocket = this.runSockets.get(run.friendlyId);
|
||||
|
||||
if (!runSocket) {
|
||||
console.debug("[WorkloadServer] notifyRun: Run socket not found", { run });
|
||||
this.logger.debug("notifyRun: Run socket not found", { run });
|
||||
|
||||
this.workerClient.sendDebugLog(run.friendlyId, {
|
||||
time: new Date(),
|
||||
@@ -556,14 +587,14 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
|
||||
}
|
||||
|
||||
runSocket.emit("run:notify", { version: "1", run });
|
||||
console.debug("[WorkloadServer] run:notify sent", { run });
|
||||
this.logger.debug("run:notify sent", { run });
|
||||
|
||||
this.workerClient.sendDebugLog(run.friendlyId, {
|
||||
time: new Date(),
|
||||
message: "run:notify supervisor -> runner",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[WorkloadServer] Error in notifyRun", { run, error });
|
||||
this.logger.error("Error in notifyRun", { run, error });
|
||||
|
||||
this.workerClient.sendDebugLog(run.friendlyId, {
|
||||
time: new Date(),
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"sourceType": "unambiguous",
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -18,4 +18,5 @@ build-storybook.log
|
||||
storybook-static
|
||||
|
||||
/prisma/seed.js
|
||||
/prisma/populate.js
|
||||
/prisma/populate.js
|
||||
.memory-snapshots
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
API_VERSION_HEADER_NAME,
|
||||
API_VERSION as CORE_API_VERSION,
|
||||
} from "@trigger.dev/core/v3/serverOnly";
|
||||
import { z } from "zod";
|
||||
|
||||
export const CURRENT_API_VERSION = CORE_API_VERSION;
|
||||
|
||||
export const NON_SPECIFIC_API_VERSION = "none";
|
||||
|
||||
export type API_VERSIONS = typeof CURRENT_API_VERSION | typeof NON_SPECIFIC_API_VERSION;
|
||||
|
||||
export function getApiVersion(request: Request): API_VERSIONS {
|
||||
const apiVersion = request.headers.get(API_VERSION_HEADER_NAME);
|
||||
|
||||
if (apiVersion === CURRENT_API_VERSION) {
|
||||
return apiVersion;
|
||||
}
|
||||
|
||||
return NON_SPECIFIC_API_VERSION;
|
||||
}
|
||||
|
||||
// This has been copied from the core package to allow us to use these types in the webapp
|
||||
export const RunStatusUnspecifiedApiVersion = z.enum([
|
||||
/// Task is waiting for a version update because it cannot execute without additional information (task, queue, etc.). Replaces WAITING_FOR_DEPLOY
|
||||
"PENDING_VERSION",
|
||||
/// Task hasn't been deployed yet but is waiting to be executed
|
||||
"WAITING_FOR_DEPLOY",
|
||||
/// Task is waiting to be executed by a worker
|
||||
"QUEUED",
|
||||
/// Task is currently being executed by a worker
|
||||
"EXECUTING",
|
||||
/// Task has failed and is waiting to be retried
|
||||
"REATTEMPTING",
|
||||
/// Task has been paused by the system, and will be resumed by the system
|
||||
"FROZEN",
|
||||
/// Task has been completed successfully
|
||||
"COMPLETED",
|
||||
/// Task has been canceled by the user
|
||||
"CANCELED",
|
||||
/// Task has been completed with errors
|
||||
"FAILED",
|
||||
/// Task has crashed and won't be retried, most likely the worker ran out of resources, e.g. memory or storage
|
||||
"CRASHED",
|
||||
/// Task was interrupted during execution, mostly this happens in development environments
|
||||
"INTERRUPTED",
|
||||
/// Task has failed to complete, due to an error in the system
|
||||
"SYSTEM_FAILURE",
|
||||
/// Task has been scheduled to run at a specific time
|
||||
"DELAYED",
|
||||
/// Task has expired and won't be executed
|
||||
"EXPIRED",
|
||||
/// Task has reached it's maxDuration and has been stopped
|
||||
"TIMED_OUT",
|
||||
]);
|
||||
|
||||
export type RunStatusUnspecifiedApiVersion = z.infer<typeof RunStatusUnspecifiedApiVersion>;
|
||||
@@ -1,53 +1,31 @@
|
||||
export function AISparkleIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.9806 0.803884C14.8871 0.33646 14.4767 0 14 0C13.5233 0 13.1129 0.33646 13.0194 0.803884L12.7809 1.99644C12.7017 2.3923 12.3923 2.70174 11.9964 2.78091L10.8039 3.01942C10.3365 3.1129 10 3.52332 10 4C10 4.47668 10.3365 4.8871 10.8039 4.98058L11.9964 5.21909C12.3923 5.29826 12.7017 5.6077 12.7809 6.00356L13.0194 7.19612C13.1129 7.66354 13.5233 8 14 8C14.4767 8 14.8871 7.66354 14.9806 7.19612L15.2191 6.00356C15.2983 5.6077 15.6077 5.29826 16.0036 5.21909L17.1961 4.98058C17.6635 4.8871 18 4.47668 18 4C18 3.52332 17.6635 3.1129 17.1961 3.01942L16.0036 2.78091C15.6077 2.70174 15.2983 2.3923 15.2191 1.99644L14.9806 0.803884Z"
|
||||
fill="url(#paint0_linear_11402_36656)"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.14286 4.85718C9.46177 4.85718 9.74205 5.06859 9.82966 5.37523L10.6041 8.08589C10.9431 9.27235 11.8705 10.1998 13.057 10.5388L15.7677 11.3132C16.0743 11.4008 16.2857 11.6811 16.2857 12C16.2857 12.319 16.0743 12.5992 15.7677 12.6868L13.057 13.4613C11.8705 13.8003 10.9431 14.7277 10.6041 15.9142L9.82966 18.6248C9.74205 18.9315 9.46177 19.1429 9.14286 19.1429C8.82394 19.1429 8.54367 18.9315 8.45605 18.6248L7.68158 15.9142C7.34259 14.7277 6.41517 13.8003 5.22871 13.4613L2.51806 12.6868C2.21141 12.5992 2 12.319 2 12C2 11.6811 2.21141 11.4008 2.51806 11.3132L5.22871 10.5388C6.41517 10.1998 7.34259 9.27235 7.68158 8.08589L8.45605 5.37523C8.54367 5.06859 8.82394 4.85718 9.14286 4.85718Z"
|
||||
fill="#7655FD"
|
||||
/>
|
||||
<path
|
||||
d="M5.94868 4.68377C5.81257 4.27543 5.43043 4 5 4C4.56957 4 4.18743 4.27543 4.05132 4.68377L3.36754 6.73509C3.26801 7.03369 3.03369 7.26801 2.73509 7.36754L0.683772 8.05132C0.27543 8.18743 0 8.56957 0 9C0 9.43043 0.27543 9.81257 0.683772 9.94868L2.73509 10.6325C3.03369 10.732 3.26801 10.9663 3.36754 11.2649L4.05132 13.3162C4.18743 13.7246 4.56957 14 5 14C5.43043 14 5.81257 13.7246 5.94868 13.3162L6.63246 11.2649C6.73199 10.9663 6.96631 10.732 7.26491 10.6325L9.31623 9.94868C9.72457 9.81257 10 9.43043 10 9C10 8.56957 9.72457 8.18743 9.31623 8.05132L7.26491 7.36754C6.96631 7.26801 6.73199 7.03369 6.63246 6.73509L5.94868 4.68377Z"
|
||||
fill="url(#paint1_linear_11402_36656)"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.7143 2C18.0421 2 18.3278 2.22307 18.4072 2.54105L18.6538 3.5272C18.8777 4.42291 19.5771 5.12229 20.4728 5.34622L21.459 5.59276C21.7769 5.67225 22 5.95795 22 6.28571C22 6.61348 21.7769 6.89918 21.459 6.97867L20.4728 7.22521C19.5771 7.44914 18.8777 8.14851 18.6538 9.04423L18.4072 10.0304C18.3278 10.3484 18.0421 10.5714 17.7143 10.5714C17.3865 10.5714 17.1008 10.3484 17.0213 10.0304L16.7748 9.04423C16.5509 8.14852 15.8515 7.44914 14.9558 7.22521L13.9696 6.97867C13.6516 6.89918 13.4286 6.61348 13.4286 6.28571C13.4286 5.95795 13.6516 5.67225 13.9696 5.59276L14.9558 5.34622C15.8515 5.12229 16.5509 4.42291 16.7748 3.5272L17.0213 2.54105C17.1008 2.22307 17.3865 2 17.7143 2Z"
|
||||
fill="#D946EF"
|
||||
/>
|
||||
<path
|
||||
d="M12.9487 12.6838C12.8126 12.2754 12.4304 12 12 12C11.5696 12 11.1874 12.2754 11.0513 12.6838L10.8675 13.2351C10.768 13.5337 10.5337 13.768 10.2351 13.8675L9.68377 14.0513C9.27543 14.1874 9 14.5696 9 15C9 15.4304 9.27543 15.8126 9.68377 15.9487L10.2351 16.1325C10.5337 16.232 10.768 16.4663 10.8675 16.7649L11.0513 17.3162C11.1874 17.7246 11.5696 18 12 18C12.4304 18 12.8126 17.7246 12.9487 17.3162L13.1325 16.7649C13.232 16.4663 13.4663 16.232 13.7649 16.1325L14.3162 15.9487C14.7246 15.8126 15 15.4304 15 15C15 14.5696 14.7246 14.1874 14.3162 14.0513L13.7649 13.8675C13.4663 13.768 13.232 13.5337 13.1325 13.2351L12.9487 12.6838Z"
|
||||
fill="url(#paint2_linear_11402_36656)"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16.2857 14.8572C16.5932 14.8572 16.8661 15.0539 16.9633 15.3456L17.3388 16.472C17.481 16.8986 17.8157 17.2333 18.2423 17.3755L19.3687 17.751C19.6604 17.8482 19.8571 18.1212 19.8571 18.4286C19.8571 18.7361 19.6604 19.009 19.3687 19.1062L18.2423 19.4817C17.8157 19.6239 17.481 19.9586 17.3388 20.3852L16.9633 21.5116C16.8661 21.8033 16.5932 22 16.2857 22C15.9783 22 15.7053 21.8033 15.6081 21.5116L15.2326 20.3852C15.0904 19.9586 14.7557 19.6239 14.3291 19.4817L13.2027 19.1062C12.911 19.009 12.7143 18.7361 12.7143 18.4286C12.7143 18.1212 12.911 17.8482 13.2027 17.751L14.3291 17.3755C14.7557 17.2333 15.0904 16.8986 15.2326 16.472L15.6081 15.3456C15.7053 15.0539 15.9783 14.8572 16.2857 14.8572Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_11402_36656"
|
||||
x1="9"
|
||||
y1="0"
|
||||
x2="9"
|
||||
y2="18"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E543FF" />
|
||||
<stop offset="1" stopColor="#286399" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_11402_36656"
|
||||
x1="9"
|
||||
y1="0"
|
||||
x2="9"
|
||||
y2="18"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E543FF" />
|
||||
<stop offset="1" stopColor="#286399" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_11402_36656"
|
||||
x1="9"
|
||||
y1="0"
|
||||
x2="9"
|
||||
y2="18"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E543FF" />
|
||||
<stop offset="1" stopColor="#286399" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
export function ArchiveIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_17398_782)">
|
||||
<path
|
||||
d="M19.2293 9C20.1562 9.00001 20.8612 9.83278 20.7088 10.7471L19.2781 19.3291C19.1173 20.2933 18.283 21 17.3055 21H6.69416C5.71662 21 4.88235 20.2933 4.7215 19.3291L3.29084 10.7471C3.13848 9.83289 3.8436 9.00021 4.77033 9H19.2293ZM9.95002 12.5C9.12179 12.5002 8.45002 13.1717 8.45002 14C8.45002 14.8283 9.12179 15.4998 9.95002 15.5H13.95L14.1033 15.4922C14.8597 15.4154 15.45 14.7767 15.45 14C15.45 13.2233 14.8597 12.5846 14.1033 12.5078L13.95 12.5H9.95002Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect x="2" y="3" width="20" height="4" rx="1" fill="currentColor" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_17398_782">
|
||||
<rect width="24" height="24" fill="currentColor" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function UnarchiveIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_17398_66731)">
|
||||
<path
|
||||
d="M19.2027 10C20.1385 10 20.8456 10.8478 20.6782 11.7686L19.2984 19.3574C19.1254 20.3084 18.2972 21 17.3306 21H6.66945C5.70287 21 4.87458 20.3084 4.70167 19.3574L3.32179 11.7686C3.15438 10.8478 3.86152 10 4.79738 10H10.9995V16C10.9995 16.5521 11.4475 16.9997 11.9995 17C12.5518 17 12.9995 16.5523 12.9995 16V10H19.2027Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect x="11" y="4" width="2" height="6" fill="currentColor" />
|
||||
<path
|
||||
d="M15.5 6.5L12 3L8.5 6.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_17398_66731">
|
||||
<rect width="24" height="24" fill="currentColor" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
export function BunLogoIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 80 70" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="Shadow"
|
||||
d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"
|
||||
/>
|
||||
<g id="Body">
|
||||
<path
|
||||
id="Background"
|
||||
d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z"
|
||||
style={{ fill: "#fbf0df" }}
|
||||
/>
|
||||
<path
|
||||
id="Bottom_Shadow"
|
||||
data-name="Bottom Shadow"
|
||||
d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z"
|
||||
style={{ fill: "#f6dece" }}
|
||||
/>
|
||||
<path
|
||||
id="Light_Shine"
|
||||
data-name="Light Shine"
|
||||
d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z"
|
||||
style={{ fill: "#fffefc" }}
|
||||
/>
|
||||
<path
|
||||
id="Top"
|
||||
d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z"
|
||||
style={{ fill: "#ccbea7", fillRule: "evenodd" }}
|
||||
/>
|
||||
<path
|
||||
id="Outline"
|
||||
d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"
|
||||
/>
|
||||
</g>
|
||||
<g id="Mouth">
|
||||
<g id="Background-2" data-name="Background">
|
||||
<path
|
||||
d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z"
|
||||
style={{ fill: "#b71422" }}
|
||||
/>
|
||||
</g>
|
||||
<g id="Tongue">
|
||||
<path
|
||||
id="Background-3"
|
||||
data-name="Background"
|
||||
d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z"
|
||||
style={{ fill: "#ff6164" }}
|
||||
/>
|
||||
<path
|
||||
id="Outline-2"
|
||||
data-name="Outline"
|
||||
d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
id="Outline-3"
|
||||
data-name="Outline"
|
||||
d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"
|
||||
/>
|
||||
</g>
|
||||
<g id="Face">
|
||||
<ellipse
|
||||
id="Right_Blush"
|
||||
data-name="Right Blush"
|
||||
cx="53.22"
|
||||
cy="40.18"
|
||||
rx="5.85"
|
||||
ry="3.44"
|
||||
style={{ fill: "#febbd0" }}
|
||||
/>
|
||||
<ellipse
|
||||
id="Left_Bluch"
|
||||
data-name="Left Bluch"
|
||||
cx="22.95"
|
||||
cy="40.18"
|
||||
rx="5.85"
|
||||
ry="3.44"
|
||||
style={{ fill: "#febbd0" }}
|
||||
/>
|
||||
<path
|
||||
id="Eyes"
|
||||
d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z"
|
||||
style={{ fillRule: "evenodd" }}
|
||||
/>
|
||||
<path
|
||||
id="Iris"
|
||||
d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z"
|
||||
style={{ fill: "#fff", fillRule: "evenodd" }}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function ClockRotateLeftIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.01784 10.9999C4.27072 9.07068 5.21806 7.29972 6.68252 6.01856C8.14697 4.73741 10.0282 4.03389 11.9739 4.03971C13.9197 4.04553 15.7966 4.76028 17.2534 6.05017C18.7101 7.34006 19.6469 9.11666 19.8882 11.0474C20.1296 12.9781 19.659 14.9306 18.5645 16.5394C17.4701 18.1482 15.8268 19.303 13.9424 19.7876C12.0579 20.2722 10.0615 20.0534 8.32671 19.1721C6.59196 18.2909 5.23784 16.8076 4.51784 14.9999M4.01784 19.9999L4.01784 14.9999L9.01784 14.9999"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M12 12L12 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<path d="M12 12L14 14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
export function CloudProviderIcon({
|
||||
provider,
|
||||
className,
|
||||
}: {
|
||||
provider: "aws" | "digitalocean" | (string & {});
|
||||
className?: string;
|
||||
}) {
|
||||
switch (provider) {
|
||||
case "aws":
|
||||
return <AWS className={className} />;
|
||||
case "digitalocean":
|
||||
return <DigitalOcean className={className} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function AWS({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M67.6343 100.132C67.5545 102.527 67.8738 104.921 68.5123 107.236C69.2307 109.232 70.1088 111.147 71.0666 112.983C71.3859 113.542 71.6254 114.181 71.6254 114.819C71.5455 115.777 70.9868 116.735 70.1088 117.214L65.08 120.566C64.4414 120.965 63.723 121.285 63.0046 121.285C62.1266 121.205 61.2486 120.806 60.61 120.167C59.5723 119.05 58.5346 117.773 57.7364 116.416C56.9382 115.059 56.14 113.542 55.2619 111.706C49.0359 119.05 41.2134 122.722 31.7944 122.722C25.0894 122.722 19.7414 120.806 15.8301 116.974C11.9189 113.143 9.92334 108.034 9.92334 101.649C9.92334 94.8638 12.318 89.3561 17.1871 85.2054C22.0562 81.0547 28.5217 78.9793 36.7434 78.9793C39.5371 78.9793 42.4107 79.2188 45.2044 79.6179C48.1578 80.017 51.1911 80.6556 54.3839 81.374V75.547C54.3839 69.4806 53.1068 65.25 50.6323 62.7756C48.1578 60.3011 43.7677 59.1038 37.6214 59.1038C34.7478 59.1038 31.7944 59.4231 29.0007 60.1415C26.0473 60.8598 23.1737 61.7379 20.38 62.8554C19.5019 63.2545 18.5441 63.5738 17.5862 63.8931C17.1871 64.0527 16.7082 64.1325 16.3091 64.1325C15.1916 64.1325 14.6328 63.3343 14.6328 61.6581V57.7468C14.553 56.7889 14.7925 55.8311 15.1916 54.9531C15.8301 54.2347 16.5485 53.6759 17.4266 53.2768C20.6194 51.6804 23.9719 50.483 27.4841 49.6848C31.555 48.6472 35.7855 48.1682 39.9362 48.1682C49.435 48.1682 56.3795 50.3234 60.8495 54.6338C65.3195 58.9441 67.4746 65.4895 67.4746 74.2699V100.132H67.6343ZM35.2268 112.265C38.0205 112.265 40.8143 111.786 43.4484 110.828C46.4018 109.79 48.9561 108.034 51.0314 105.72C52.3086 104.283 53.2664 102.527 53.7453 100.611C54.3041 98.376 54.5436 95.9813 54.5436 93.6665V90.314C52.1489 89.7552 49.6744 89.2763 47.2 88.957C44.7255 88.6377 42.1712 88.4781 39.6968 88.4781C34.3487 88.4781 30.4375 89.5158 27.8034 91.6709C25.1692 93.8261 23.8921 96.8593 23.8921 100.85C23.8921 104.602 24.85 107.396 26.8455 109.312C28.841 111.227 31.555 112.265 35.2268 112.265ZM99.3234 120.886C98.2857 120.965 97.1682 120.726 96.2902 120.087C95.492 119.209 94.8534 118.172 94.614 116.974L75.8559 55.2723C75.4568 54.2347 75.2173 53.197 75.1375 52.0795C75.0577 51.1216 75.6963 50.2436 76.6541 50.0839H84.9556C86.4722 50.0839 87.5098 50.3234 88.0686 50.8822C88.6273 51.4409 89.1861 52.4786 89.665 53.9952L102.995 106.837L115.447 53.9952C115.846 52.3988 116.325 51.3611 116.964 50.8822C117.922 50.3234 119.039 50.0041 120.157 50.0839H126.543C128.059 50.0839 129.097 50.3234 129.735 50.8822C130.534 51.7602 131.092 52.7979 131.252 53.9952L143.864 107.476L157.673 53.9952C157.912 52.7979 158.471 51.7602 159.269 50.8822C160.227 50.3234 161.265 50.0041 162.382 50.0839H169.806C170.764 49.9243 171.642 50.6427 171.801 51.6005V52.0795C171.801 52.4786 171.722 52.9575 171.642 53.3566C171.482 54.075 171.322 54.7136 171.083 55.3522L151.846 117.054C151.367 118.651 150.808 119.688 150.17 120.167C149.292 120.726 148.174 121.045 147.137 120.965H140.272C138.755 120.965 137.718 120.726 137.079 120.167C136.281 119.289 135.722 118.172 135.562 116.974L123.19 65.4895L110.898 116.895C110.738 118.092 110.179 119.209 109.381 120.087C108.423 120.726 107.306 120.965 106.188 120.886H99.3234ZM201.894 123.041C197.743 123.041 193.593 122.562 189.602 121.604C185.61 120.646 182.497 119.608 180.422 118.411C179.384 117.932 178.506 117.134 177.948 116.176C177.628 115.458 177.469 114.739 177.469 113.941V109.87C177.469 108.194 178.107 107.396 179.305 107.396C179.784 107.396 180.262 107.476 180.741 107.635C181.22 107.795 181.939 108.114 182.737 108.433C185.531 109.711 188.484 110.589 191.517 111.227C194.63 111.866 197.823 112.185 201.016 112.185C206.045 112.185 209.956 111.307 212.67 109.551C215.304 107.955 216.901 105.081 216.821 101.968C216.901 99.8926 216.102 97.897 214.665 96.3804C213.229 94.8638 210.515 93.5068 206.604 92.2297L195.029 88.6377C189.202 86.8018 184.892 84.0879 182.258 80.4959C179.704 77.1434 178.267 73.1524 178.267 68.9218C178.187 65.8088 178.905 62.7756 180.422 60.0616C181.859 57.5073 183.775 55.2723 186.169 53.5163C188.644 51.6005 191.437 50.2436 194.471 49.3655C197.743 48.4077 201.096 47.9288 204.528 48.0086C206.284 48.0086 208.12 48.0884 209.876 48.3279C211.712 48.5673 213.388 48.8866 215.065 49.2059C216.741 49.5252 218.178 50.0041 219.614 50.483C220.812 50.8822 221.929 51.3611 222.967 51.9198C223.925 52.3988 224.723 53.0373 225.362 53.9154C225.92 54.7136 226.16 55.5916 226.08 56.5495V60.3011C226.08 61.9773 225.441 62.8554 224.244 62.8554C223.206 62.6957 222.169 62.3765 221.211 61.8975C216.422 59.7423 211.153 58.7047 205.885 58.7845C201.335 58.7845 197.743 59.5029 195.269 61.0195C192.794 62.5361 191.517 64.8509 191.517 68.1236C191.437 70.2788 192.315 72.3541 193.912 73.7909C195.508 75.3075 198.462 76.8241 202.692 78.1811L214.027 81.7731C219.774 83.609 223.925 86.1633 226.399 89.4359C228.874 92.6288 230.151 96.6199 230.071 100.611C230.151 103.804 229.432 106.997 227.996 109.87C226.559 112.584 224.563 114.979 222.169 116.895C219.535 118.97 216.501 120.487 213.309 121.365C209.637 122.482 205.805 123.041 201.894 123.041Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M216.98 161.834C190.719 181.231 152.564 191.528 119.758 191.528C73.7806 191.528 32.3533 174.526 1.06324 146.269C-1.41123 144.034 0.823772 141.001 3.77717 142.757C37.6215 162.393 79.3681 174.286 122.552 174.286C153.682 174.126 184.493 167.821 213.149 155.768C217.539 153.772 221.291 158.641 216.98 161.834Z"
|
||||
fill="#FF9900"
|
||||
/>
|
||||
<path
|
||||
d="M227.916 149.382C224.563 145.072 205.726 147.307 197.185 148.344C194.63 148.664 194.231 146.428 196.546 144.752C211.553 134.216 236.217 137.249 239.091 140.761C241.965 144.273 238.293 169.018 224.244 180.832C222.089 182.667 220.014 181.71 220.971 179.315C224.164 171.413 231.268 153.612 227.916 149.382Z"
|
||||
fill="#FF9900"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function DigitalOcean({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_18907_83670)">
|
||||
<path
|
||||
d="M120.001 218.82V180.552C160.62 180.552 192.015 140.347 176.514 97.6503C170.768 81.8384 158.157 69.2205 142.339 63.4808C99.6439 47.9862 59.4601 79.3824 59.4532 119.988L59.4463 120.002H21.1802C21.1802 55.292 83.6945 4.91045 151.493 26.0827C181.122 35.327 204.667 58.8724 213.918 88.5025C235.089 156.31 184.703 218.82 120.001 218.82Z"
|
||||
fill="#0069FF"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M120.089 180.647H81.9333V142.497L81.9402 142.49H120.082L120.089 142.497V180.647Z"
|
||||
fill="#0069FF"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M81.925 209.964H52.6132L52.6063 209.957V180.644H81.9319V209.957L81.925 209.964Z"
|
||||
fill="#0069FF"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M52.6298 180.647H28.0573L28.0435 180.64V156.081L28.0573 156.06H52.616L52.6298 156.067V180.647Z"
|
||||
fill="#0069FF"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_18907_83670">
|
||||
<rect
|
||||
width="197.64"
|
||||
height="197.64"
|
||||
fill="white"
|
||||
transform="translate(21.1802 21.1802)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export function ProdEnvironmentIcon({ className }: { className?: string }) {
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.5037 7.33603C12.3174 6.88799 11.6827 6.88799 11.4963 7.33603L10.4338 9.89064L7.6759 10.1117C7.1922 10.1505 6.99606 10.7542 7.36459 11.0698L9.46583 12.8698L8.82387 15.561C8.71128 16.033 9.22477 16.4061 9.63888 16.1532L12 14.711L14.3612 16.1532C14.7753 16.4061 15.2888 16.0331 15.1762 15.561L14.5343 12.8698L16.6355 11.0698C17.004 10.7542 16.8079 10.1505 16.3242 10.1117L13.5663 9.89064L12.5037 7.33603Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="3"
|
||||
@@ -151,3 +151,28 @@ export function DeployedEnvironmentIconSmall({ className }: { className?: string
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewEnvironmentIconSmall({ className }: { className?: string }) {
|
||||
return <BranchEnvironmentIconSmall className={className} />;
|
||||
}
|
||||
|
||||
export function BranchEnvironmentIconSmall({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.23059 7C1.65409 6.99967 1.30545 6.36259 1.61634 5.87695L3.88489 2.33301C4.17195 1.88466 4.82727 1.88472 5.11438 2.33301L7.38294 5.87695C7.69386 6.36265 7.34531 6.99982 6.76868 7L2.23059 7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 6H5.5V15C5.5 15.5523 5.05228 16 4.5 16C3.94772 16 3.5 15.5523 3.5 15V6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13.5 7V7.5C13.5 9.15685 12.1569 10.5 10.5 10.5H7.5C5.84315 10.5 4.5 11.8431 4.5 13.5V15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<circle cx="13.5" cy="5" r="2" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { LogoIcon } from "~/components/LogoIcon";
|
||||
|
||||
export function IntegrationIcon() {
|
||||
return <LogoIcon className="h-3.5 w-3.5 flex-none pb-0.5" />;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function KeyboardEnterIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 5H18C19.6569 5 21 6.34315 21 8V13C21 14.6569 19.6569 16 18 16H8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path d="M2 16L8 12L8 20L2 16Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
export function ListCheckedIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.85001 3.82759L4.09139 5.06897L6.16035 3"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.65517"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.85001 8.82759L4.09139 10.069L6.16035 8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.65517"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.85001 13.8276L4.09139 15.069L6.16035 13"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.65517"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 4H15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 9H15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 14H15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function MachineIcon({ preset, className }: { preset?: string; className?: string }) {
|
||||
if (!preset) {
|
||||
return <MachineIconNoMachine className={cn("size-5", className)} />;
|
||||
}
|
||||
|
||||
switch (preset) {
|
||||
case "no-machine":
|
||||
return <MachineIconNoMachine className={cn("size-5", className)} />;
|
||||
case "micro":
|
||||
return <MachineIconMicro className={cn("size-5", className)} />;
|
||||
case "small-1x":
|
||||
return <MachineIconSmall1x className={cn("size-5", className)} />;
|
||||
case "small-2x":
|
||||
return <MachineIconSmall2x className={cn("size-5", className)} />;
|
||||
case "medium-1x":
|
||||
return <MachineIconMedium1x className={cn("size-5", className)} />;
|
||||
case "medium-2x":
|
||||
return <MachineIconMedium2x className={cn("size-5", className)} />;
|
||||
case "large-1x":
|
||||
return <MachineIconLarge1x className={cn("size-5", className)} />;
|
||||
case "large-2x":
|
||||
return <MachineIconLarge2x className={cn("size-5", className)} />;
|
||||
default:
|
||||
return <MachineDefaultIcon className={cn("size-5", className)} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function MachineDefaultIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconNoMachine({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H6.18945C6.25882 16.9453 6.32663 16.8872 6.39062 16.8232L10.2139 13H16.1875Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M4.84961 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H8.84961L4.84961 12ZM16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H11.2139L15.2139 8H16.1875Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M9.85059 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H13.8506L9.85059 7Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
<line
|
||||
x1="3"
|
||||
y1="16.9998"
|
||||
x2="17.0018"
|
||||
y2="2.99805"
|
||||
stroke="#D7D9DD"
|
||||
strokeOpacity="0.4"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconMicro({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconSmall1x({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconSmall2x({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconMedium1x({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconMedium2x({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="#D7D9DD"
|
||||
fillOpacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconLarge1x({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MachineIconLarge2x({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.1875 13C16.6362 13 17 13.3638 17 13.8125V16.1875C17 16.6362 16.6362 17 16.1875 17H3.8125C3.36377 17 3 16.6362 3 16.1875V13.8125C3 13.3638 3.36377 13 3.8125 13H16.1875ZM5 14C4.44772 14 4 14.4477 4 15C4 15.5523 4.44772 16 5 16C5.55228 16 6 15.5523 6 15C6 14.4477 5.55228 14 5 14Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 8C16.6362 8 17 8.36377 17 8.8125V11.1875C17 11.6362 16.6362 12 16.1875 12H3.8125C3.36377 12 3 11.6362 3 11.1875V8.8125C3 8.36377 3.36377 8 3.8125 8H16.1875ZM5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
<path
|
||||
d="M16.1875 3C16.6362 3 17 3.36377 17 3.8125V6.1875C17 6.63623 16.6362 7 16.1875 7H3.8125C3.36377 7 3 6.63623 3 6.1875V3.8125C3 3.36377 3.36377 3 3.8125 3H16.1875ZM5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4Z"
|
||||
fill="#3B82F6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function NodejsLogoIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 71 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_337_7891)" fill="#5FA04E">
|
||||
<path d="M35.625 79.5c-1.081 0-2.09-.288-3.028-.792l-9.59-5.686c-1.442-.792-.721-1.08-.289-1.224 1.947-.648 2.308-.792 4.327-1.944.216-.144.504-.072.72.072l7.356 4.391c.288.144.649.144.865 0l28.77-16.628c.289-.144.433-.431.433-.791V23.714c0-.36-.144-.648-.432-.792L35.986 6.366c-.288-.144-.65-.144-.865 0L6.35 22.922c-.29.144-.434.504-.434.792v33.184c0 .287.145.647.433.791l7.86 4.535c4.254 2.16 6.922-.36 6.922-2.879V26.593c0-.432.36-.864.865-.864h3.678c.432 0 .865.36.865.864v32.752c0 5.687-3.1 8.998-8.509 8.998-1.658 0-2.956 0-6.633-1.8l-7.572-4.319A6.073 6.073 0 0 1 .798 56.97V23.786a6.073 6.073 0 0 1 3.028-5.255l28.77-16.628c1.804-1.008 4.255-1.008 6.058 0l28.77 16.628a6.073 6.073 0 0 1 3.029 5.255V56.97a6.073 6.073 0 0 1-3.029 5.254l-28.77 16.628c-.865.36-1.947.648-3.029.648Z" />
|
||||
<path d="M44.567 56.682c-12.62 0-15.215-5.759-15.215-10.654 0-.432.36-.864.865-.864h3.75c.433 0 .793.288.793.72.577 3.815 2.235 5.687 9.879 5.687 6.057 0 8.652-1.368 8.652-4.607 0-1.871-.72-3.24-10.167-4.175-7.86-.792-12.762-2.52-12.762-8.782 0-5.83 4.903-9.285 13.123-9.285 9.23 0 13.772 3.167 14.35 10.077 0 .216-.073.432-.217.648-.144.144-.36.288-.577.288h-3.822a.844.844 0 0 1-.793-.648c-.865-3.96-3.1-5.255-9.013-5.255-6.634 0-7.427 2.304-7.427 4.031 0 2.088.937 2.736 9.879 3.887 8.869 1.152 13.05 2.808 13.05 8.998 0 6.335-5.263 9.934-14.348 9.934Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_337_7891">
|
||||
<path fill="#fff" d="M0 .5h71v79H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
export function FlagIcon({
|
||||
region,
|
||||
className,
|
||||
}: {
|
||||
region: "usa" | "europe" | (string & {});
|
||||
className?: string;
|
||||
}) {
|
||||
switch (region) {
|
||||
case "usa":
|
||||
return <FlagUSA className={className} />;
|
||||
case "europe":
|
||||
return <FlagEurope className={className} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function FlagUSA({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 96 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_18908_83688)">
|
||||
<rect x="0.09375" width="95.9062" height="71.9297" rx="5.99414" fill="white" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.09375 0V8.0065H96.0937V0H0.09375Z"
|
||||
fill="#E31D1C"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.09375 16.0131V24.0196H96.0937V16.0131H0.09375Z"
|
||||
fill="#E31D1C"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.09375 31.9791V39.9856H96.0937V31.9791H0.09375Z"
|
||||
fill="#E31D1C"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.09375 47.9921V55.9986H96.0937V47.9921H0.09375Z"
|
||||
fill="#E31D1C"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.09375 63.9584V71.9649H96.0937V63.9584H0.09375Z"
|
||||
fill="#E31D1C"
|
||||
/>
|
||||
<rect x="0.09375" width="60" height="40" fill="#2E52B2" />
|
||||
<path
|
||||
d="M50.9424 29.8628H53.0938L51.4033 31.5835L52.0576 34.2974L49.9531 32.772L47.7773 34.2974L48.5117 31.5835L46.5791 29.8628H49.1035L49.9502 27.6479L50.9424 29.8628ZM11.3486 29.2515H13.5L11.8096 30.9712L12.4639 33.686L10.3594 32.1606L8.18359 33.686L8.91797 30.9712L6.98633 29.2515H9.50977L10.3564 27.0366L11.3486 29.2515ZM24.5469 29.2515H26.6982L25.0078 30.9712L25.6621 33.686L23.5566 32.1606L21.3818 33.686L22.1162 30.9712L20.1836 29.2515H22.708L23.5537 27.0366L24.5469 29.2515ZM37.7441 29.2515H39.8955L38.2051 30.9712L38.8604 33.686L36.7549 32.1606L34.5791 33.686L35.3135 30.9712L33.3818 29.2515H35.9053L36.752 27.0366L37.7441 29.2515ZM50.9424 18.9077H53.0938L51.4033 20.6284L52.0576 23.3423L49.9521 21.8169L47.7773 23.3423L48.5117 20.6284L46.5791 18.9077H49.1025L49.9492 16.6929L50.9424 18.9077ZM11.3486 18.6021H13.5L11.8096 20.3218L12.4639 23.0366L10.3594 21.5112L8.18359 23.0366L8.91797 20.3218L6.98535 18.6021H9.50977L10.3555 16.3872L11.3486 18.6021ZM24.5459 18.6021H26.6973L25.0068 20.3218L25.6621 23.0366L23.5566 21.5112L21.3809 23.0366L22.1152 20.3218L20.1836 18.6021H22.707L23.5537 16.3872L24.5459 18.6021ZM37.7441 18.6021H39.8955L38.2051 20.3218L38.8594 23.0366L36.7549 21.5112L34.5791 23.0366L35.3135 20.3218L33.3809 18.6021H35.9053L36.752 16.3872L37.7441 18.6021ZM11.3486 7.95264H13.5L11.8096 9.67334L12.4639 12.3872L10.3594 10.8618L8.18359 12.3872L8.91797 9.67334L6.98535 7.95264H9.50977L10.3555 5.73779L11.3486 7.95264ZM24.5459 7.95264H26.6973L25.0068 9.67334L25.6621 12.3872L23.5566 10.8618L21.3809 12.3872L22.1152 9.67334L20.1836 7.95264H22.707L23.5537 5.73779L24.5459 7.95264ZM37.7441 7.95264H39.8955L38.2051 9.67334L38.8594 12.3872L36.7549 10.8618L34.5791 12.3872L35.3135 9.67334L33.3809 7.95264H35.9053L36.752 5.73779L37.7441 7.95264ZM50.9424 7.95264H53.0938L51.4033 9.67334L52.0576 12.3872L49.9521 10.8618L47.7773 12.3872L48.5117 9.67334L46.5791 7.95264H49.1025L49.9492 5.73779L50.9424 7.95264Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_18908_83688">
|
||||
<rect x="0.09375" width="95.9062" height="71.9297" rx="5.99414" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function FlagEurope({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 96 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.09375" width="95.9062" height="71.9297" rx="5.99414" fill="#2E52B2" />
|
||||
<path
|
||||
d="M30.1359 9.60876L28.2171 15.508H22.017L27.0358 19.1573L25.1171 25.0565L30.1359 21.4072L35.1547 25.0565L33.236 19.1573L38.2548 15.508H32.0546L30.1359 9.60876Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
<path
|
||||
d="M48.0469 2.29382L46.1282 8.19302H39.928L44.9468 11.8424L43.0281 17.7416L48.0469 14.0922L53.0657 17.7416L51.147 11.8424L56.1658 8.19302H49.9656L48.0469 2.29382Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
<path
|
||||
d="M65.9577 10.0177L64.039 15.9169H57.8388L62.8576 19.5663L60.9389 25.4655L65.9577 21.8161L70.9766 25.4655L69.0578 19.5663L74.0766 15.9169H67.8765L65.9577 10.0177Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
<path
|
||||
d="M73.5861 27.4231L71.6673 33.3223H65.4672L70.486 36.9717L68.5672 42.8709L73.5861 39.2215L78.6049 42.8709L76.6862 36.9717L81.705 33.3223H75.5048L73.5861 27.4231Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
<path
|
||||
d="M65.9577 46.0563L64.039 51.9555H57.8388L62.8576 55.6048L60.9389 61.504L65.9577 57.8547L70.9766 61.504L69.0578 55.6048L74.0766 51.9555H67.8765L65.9577 46.0563Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
<path
|
||||
d="M48.0469 53.7802L46.1282 59.6794H39.928L44.9468 63.3287L43.0281 69.2279L48.0469 65.5785L53.0657 69.2279L51.147 63.3287L56.1658 59.6794H49.9656L48.0469 53.7802Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
<path
|
||||
d="M30.1359 46.0563L28.2171 51.9555H22.017L27.0358 55.6048L25.1171 61.504L30.1359 57.8547L35.1547 61.504L33.236 55.6048L38.2548 51.9555H32.0546L30.1359 46.0563Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
<path
|
||||
d="M22.5077 27.4231L20.589 33.3223H14.3888L19.4076 36.9717L17.4889 42.8709L22.5077 39.2215L27.5266 42.8709L25.6078 36.9717L30.6266 33.3223H24.4265L22.5077 27.4231Z"
|
||||
fill="#F6CD46"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export function SparkleListIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17 11C17.4304 11 17.8126 11.2754 17.9487 11.6838L18.4743 13.2607C18.6734 13.858 19.142 14.3266 19.7393 14.5257L21.3162 15.0513C21.7246 15.1874 22 15.5696 22 16C22 16.4304 21.7246 16.8126 21.3162 16.9487L19.7393 17.4743C19.142 17.6734 18.6734 18.142 18.4743 18.7393L17.9487 20.3162C17.8126 20.7246 17.4304 21 17 21C16.5696 21 16.1874 20.7246 16.0513 20.3162L15.5257 18.7393C15.3266 18.142 14.858 17.6734 14.2607 17.4743L12.6838 16.9487C12.2754 16.8126 12 16.4304 12 16C12 15.5696 12.2754 15.1874 12.6838 15.0513L14.2607 14.5257C14.858 14.3266 15.3266 13.858 15.5257 13.2607L16.0513 11.6838C16.1874 11.2754 16.5696 11 17 11Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect x="2" y="6" width="20" height="2" rx="1" fill="currentColor" />
|
||||
<rect x="2" y="15" width="7" height="2" rx="1" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export function TextInlineIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 3V21"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13 21L13 16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13 8L13 3"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7 12L20 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M17.5 15.5L21 12L17.5 8.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export function TextWrapIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 3V21"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M21 21V3"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7 7H14C15.6569 7 17 8.34315 17 10V13C17 14.6569 15.6569 16 14 16H9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11 13L8 16L11 19"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,75 @@
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import { prisma } from "./db.server";
|
||||
import { env } from "./env.server";
|
||||
import { WorkerGroupService } from "./v3/services/worker/workerGroupService.server";
|
||||
import { dirname } from "path";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
|
||||
export async function bootstrap() {
|
||||
if (env.TRIGGER_BOOTSTRAP_ENABLED !== "1") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (env.TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME) {
|
||||
const [error] = await tryCatch(createWorkerGroup());
|
||||
if (error) {
|
||||
console.error("Failed to create worker group", { error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createWorkerGroup() {
|
||||
const workerGroupName = env.TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME;
|
||||
const tokenPath = env.TRIGGER_BOOTSTRAP_WORKER_TOKEN_PATH;
|
||||
|
||||
const existingWorkerGroup = await prisma.workerInstanceGroup.findFirst({
|
||||
where: {
|
||||
name: workerGroupName,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingWorkerGroup) {
|
||||
console.warn(`[bootstrap] Worker group ${workerGroupName} already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
const service = new WorkerGroupService();
|
||||
const { token, workerGroup } = await service.createWorkerGroup({
|
||||
name: workerGroupName,
|
||||
});
|
||||
|
||||
console.log(`
|
||||
==========================
|
||||
Trigger.dev Bootstrap - Worker Token
|
||||
|
||||
WARNING: This will only be shown once. Save it now!
|
||||
|
||||
Worker group:
|
||||
${workerGroup.name}
|
||||
|
||||
Token:
|
||||
${token.plaintext}
|
||||
|
||||
If using docker compose, set:
|
||||
TRIGGER_WORKER_TOKEN=${token.plaintext}
|
||||
|
||||
${
|
||||
tokenPath
|
||||
? `Or, if using a file:
|
||||
TRIGGER_WORKER_TOKEN=file://${tokenPath}`
|
||||
: ""
|
||||
}
|
||||
|
||||
==========================
|
||||
`);
|
||||
|
||||
if (tokenPath) {
|
||||
const dir = dirname(tokenPath);
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(tokenPath, token.plaintext, {
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
console.log(`[bootstrap] Worker token saved to ${tokenPath}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ArrowUpIcon,
|
||||
HandThumbDownIcon,
|
||||
HandThumbUpIcon,
|
||||
StopIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { type FeedbackComment, KapaProvider, type QA, useChat } from "@kapaai/react-sdk";
|
||||
import { useSearchParams } from "@remix-run/react";
|
||||
import DOMPurify from "dompurify";
|
||||
import { motion } from "framer-motion";
|
||||
import { marked } from "marked";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTypedRouteLoaderData } from "remix-typedjson";
|
||||
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
|
||||
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { type loader } from "~/root";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { Callout } from "./primitives/Callout";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog";
|
||||
import { Header2 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { ShortcutKey } from "./primitives/ShortcutKey";
|
||||
import { Spinner } from "./primitives/Spinner";
|
||||
import {
|
||||
SimpleTooltip,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./primitives/Tooltip";
|
||||
import { ClientOnly } from "remix-utils/client-only";
|
||||
|
||||
function useKapaWebsiteId() {
|
||||
const routeMatch = useTypedRouteLoaderData<typeof loader>("root");
|
||||
return routeMatch?.kapa.websiteId;
|
||||
}
|
||||
|
||||
export function AskAI() {
|
||||
const { isManagedCloud } = useFeatures();
|
||||
const websiteId = useKapaWebsiteId();
|
||||
|
||||
if (!isManagedCloud || !websiteId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientOnly
|
||||
fallback={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
data-action="ask-ai"
|
||||
hideShortcutKey
|
||||
data-modal-override-open-class-ask-ai="true"
|
||||
disabled
|
||||
>
|
||||
<AISparkleIcon className="size-5" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{() => <AskAIProvider websiteId={websiteId} />}
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
type AskAIProviderProps = {
|
||||
websiteId: string;
|
||||
};
|
||||
|
||||
function AskAIProvider({ websiteId }: AskAIProviderProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [initialQuery, setInitialQuery] = useState<string | undefined>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const openAskAI = useCallback((question?: string) => {
|
||||
if (question) {
|
||||
setInitialQuery(question);
|
||||
} else {
|
||||
setInitialQuery(undefined);
|
||||
}
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeAskAI = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
setInitialQuery(undefined);
|
||||
}, []);
|
||||
|
||||
// Handle URL param functionality
|
||||
useEffect(() => {
|
||||
const aiHelp = searchParams.get("aiHelp");
|
||||
if (aiHelp) {
|
||||
// Delay to avoid hCaptcha bot detection
|
||||
window.setTimeout(() => openAskAI(aiHelp), 1000);
|
||||
|
||||
// Clone instead of mutating in place
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete("aiHelp");
|
||||
setSearchParams(next);
|
||||
}
|
||||
}, [searchParams, openAskAI]);
|
||||
|
||||
return (
|
||||
<KapaProvider
|
||||
integrationId={websiteId}
|
||||
callbacks={{
|
||||
askAI: {
|
||||
onQuerySubmit: () => openAskAI(),
|
||||
onAnswerGenerationCompleted: () => openAskAI(),
|
||||
},
|
||||
}}
|
||||
botProtectionMechanism="hcaptcha"
|
||||
>
|
||||
<TooltipProvider disableHoverableContent>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex">
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
data-action="ask-ai"
|
||||
shortcut={{ modifiers: ["mod"], key: "/", enabledOnInputElements: true }}
|
||||
hideShortcutKey
|
||||
data-modal-override-open-class-ask-ai="true"
|
||||
onClick={() => openAskAI()}
|
||||
>
|
||||
<AISparkleIcon className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="flex items-center gap-1 py-1.5 pl-2.5 pr-2 text-xs">
|
||||
Ask AI
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"], key: "/" }} variant="medium/bright" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<AskAIDialog
|
||||
initialQuery={initialQuery}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
closeAskAI={closeAskAI}
|
||||
/>
|
||||
</KapaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type AskAIDialogProps = {
|
||||
initialQuery?: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
closeAskAI: () => void;
|
||||
};
|
||||
|
||||
function AskAIDialog({ initialQuery, isOpen, onOpenChange, closeAskAI }: AskAIDialogProps) {
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
closeAskAI();
|
||||
} else {
|
||||
onOpenChange(open);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="animated-gradient-glow flex max-h-[90vh] min-h-fit w-full flex-col justify-between gap-0 px-0 pb-0 pt-0 sm:max-w-prose">
|
||||
<DialogHeader className="flex h-[2.75rem] items-start justify-center rounded-t-md bg-background-bright pl-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<AISparkleIcon className="size-5" />
|
||||
<DialogTitle className="text-sm font-medium text-text-bright">Ask AI</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<ChatInterface initialQuery={initialQuery} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatMessages({
|
||||
conversation,
|
||||
isPreparingAnswer,
|
||||
isGeneratingAnswer,
|
||||
onReset,
|
||||
onExampleClick,
|
||||
error,
|
||||
addFeedback,
|
||||
}: {
|
||||
conversation: QA[];
|
||||
isPreparingAnswer: boolean;
|
||||
isGeneratingAnswer: boolean;
|
||||
onReset: () => void;
|
||||
onExampleClick: (question: string) => void;
|
||||
error: string | null;
|
||||
addFeedback: (
|
||||
questionAnswerId: string,
|
||||
reaction: "upvote" | "downvote",
|
||||
comment?: FeedbackComment
|
||||
) => void;
|
||||
}) {
|
||||
const [feedbackGivenForQAs, setFeedbackGivenForQAs] = useState<Set<string>>(new Set());
|
||||
|
||||
// Reset feedback state when conversation is reset
|
||||
useEffect(() => {
|
||||
if (conversation.length === 0) {
|
||||
setFeedbackGivenForQAs(new Set());
|
||||
}
|
||||
}, [conversation.length]);
|
||||
|
||||
// Check if feedback has been given for the latest QA
|
||||
const latestQA = conversation[conversation.length - 1];
|
||||
const hasFeedbackForLatestQA = latestQA?.id ? feedbackGivenForQAs.has(latestQA.id) : false;
|
||||
|
||||
const exampleQuestions = [
|
||||
"How do I increase my concurrency limit?",
|
||||
"How do I debug errors in my task?",
|
||||
"How do I deploy my task?",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
{conversation.length === 0 ? (
|
||||
<motion.div
|
||||
className="flex flex-col gap-2 pb-2"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Paragraph className="mb-3 mt-1.5 pl-1">
|
||||
I'm trained on docs, examples, and other content. Ask me anything about Trigger.dev.
|
||||
</Paragraph>
|
||||
{exampleQuestions.map((question, index) => (
|
||||
<motion.button
|
||||
key={index}
|
||||
className="group flex w-fit items-center gap-2 rounded-full border border-dashed border-charcoal-600 px-4 py-2 transition-colors hover:border-solid hover:border-indigo-500"
|
||||
onClick={() => onExampleClick(question)}
|
||||
variants={{
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
x: 20,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
opacity: {
|
||||
duration: 0.5,
|
||||
ease: "linear",
|
||||
},
|
||||
x: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SparkleListIcon className="size-4 text-text-dimmed transition group-hover:text-indigo-500" />
|
||||
<Paragraph variant="small" className="transition group-hover:text-text-bright">
|
||||
{question}
|
||||
</Paragraph>
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
) : (
|
||||
conversation.map((qa) => (
|
||||
<div key={qa.id || `temp-${qa.question}`} className="mb-4">
|
||||
<Header2 spacing>{qa.question}</Header2>
|
||||
<div
|
||||
className="prose prose-invert max-w-none text-text-dimmed"
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked(qa.answer)) }}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{conversation.length > 0 &&
|
||||
!isPreparingAnswer &&
|
||||
!isGeneratingAnswer &&
|
||||
!error &&
|
||||
!latestQA?.id && (
|
||||
<div className="flex items-center justify-between border-t border-grid-bright pt-3">
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
Answer generation was stopped
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
LeadingIcon={<ArrowPathIcon className="size-4" />}
|
||||
onClick={onReset}
|
||||
className="w-fit pl-1.5"
|
||||
iconSpacing="gap-x-1.5"
|
||||
>
|
||||
Reset chat
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{conversation.length > 0 &&
|
||||
!isPreparingAnswer &&
|
||||
!isGeneratingAnswer &&
|
||||
!error &&
|
||||
latestQA?.id && (
|
||||
<div className="flex items-center justify-between border-t border-grid-bright pt-3">
|
||||
{hasFeedbackForLatestQA ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
Thanks for your feedback!
|
||||
</Paragraph>
|
||||
</motion.div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
Was this helpful?
|
||||
</Paragraph>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
onClick={() => {
|
||||
const latestQA = conversation[conversation.length - 1];
|
||||
if (latestQA?.id) {
|
||||
addFeedback(latestQA.id, "upvote");
|
||||
setFeedbackGivenForQAs((prev) => new Set(prev).add(latestQA.id));
|
||||
}
|
||||
}}
|
||||
className="size-8 px-1.5"
|
||||
>
|
||||
<HandThumbUpIcon className="size-4 text-text-dimmed transition group-hover/button:text-success" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
onClick={() => {
|
||||
const latestQA = conversation[conversation.length - 1];
|
||||
if (latestQA?.id) {
|
||||
addFeedback(latestQA.id, "downvote");
|
||||
setFeedbackGivenForQAs((prev) => new Set(prev).add(latestQA.id));
|
||||
}
|
||||
}}
|
||||
className="size-8 px-1.5"
|
||||
>
|
||||
<HandThumbDownIcon className="size-4 text-text-dimmed transition group-hover/button:text-error" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
LeadingIcon={<ArrowPathIcon className="size-4" />}
|
||||
onClick={onReset}
|
||||
className="w-fit pl-1.5"
|
||||
iconSpacing="gap-x-1.5"
|
||||
>
|
||||
Reset chat
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isPreparingAnswer && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Spinner
|
||||
color={{
|
||||
background: "rgba(99, 102, 241, 1)",
|
||||
foreground: "rgba(217, 70, 239, 1)",
|
||||
}}
|
||||
className="size-4"
|
||||
/>
|
||||
<Paragraph className="text-text-dimmed">Preparing answer…</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex flex-col">
|
||||
<Callout variant="error" className="mb-4">
|
||||
<Paragraph className="font-semibold text-error">Error generating answer:</Paragraph>
|
||||
<Paragraph className="text-rose-300">
|
||||
{error} If the problem persists after retrying, please contact support.
|
||||
</Paragraph>
|
||||
</Callout>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
LeadingIcon={<ArrowPathIcon className="size-4" />}
|
||||
onClick={onReset}
|
||||
className="w-fit pl-1.5"
|
||||
iconSpacing="gap-x-1.5"
|
||||
>
|
||||
Reset chat
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatInterface({ initialQuery }: { initialQuery?: string }) {
|
||||
const [message, setMessage] = useState("");
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const hasSubmittedInitialQuery = useRef(false);
|
||||
const {
|
||||
conversation,
|
||||
submitQuery,
|
||||
isGeneratingAnswer,
|
||||
isPreparingAnswer,
|
||||
resetConversation,
|
||||
stopGeneration,
|
||||
error,
|
||||
addFeedback,
|
||||
} = useChat();
|
||||
|
||||
useEffect(() => {
|
||||
if (initialQuery && !hasSubmittedInitialQuery.current) {
|
||||
hasSubmittedInitialQuery.current = true;
|
||||
setIsExpanded(true);
|
||||
submitQuery(initialQuery);
|
||||
}
|
||||
}, [initialQuery, submitQuery]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (message.trim()) {
|
||||
setIsExpanded(true);
|
||||
submitQuery(message);
|
||||
setMessage("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleExampleClick = (question: string) => {
|
||||
setIsExpanded(true);
|
||||
submitQuery(question);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
resetConversation();
|
||||
setIsExpanded(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex h-full max-h-[90vh] grow flex-col overflow-y-auto rounded-b-md bg-background-bright"
|
||||
animate={{ height: isExpanded ? "90vh" : "auto" }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
initial={{ height: "auto" }}
|
||||
>
|
||||
<ChatMessages
|
||||
conversation={conversation}
|
||||
isPreparingAnswer={isPreparingAnswer}
|
||||
isGeneratingAnswer={isGeneratingAnswer}
|
||||
onReset={handleReset}
|
||||
onExampleClick={handleExampleClick}
|
||||
error={error}
|
||||
addFeedback={addFeedback}
|
||||
/>
|
||||
<form onSubmit={handleSubmit} className="flex-shrink-0 border-t border-grid-bright p-4">
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Ask a question..."
|
||||
disabled={isGeneratingAnswer}
|
||||
autoFocus
|
||||
className="flex-1 rounded-md border border-grid-bright bg-background-dimmed px-3 py-2 text-text-bright placeholder:text-text-dimmed focus-visible:focus-custom"
|
||||
/>
|
||||
{isGeneratingAnswer ? (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<span
|
||||
onClick={() => stopGeneration()}
|
||||
className="group relative z-10 flex size-10 min-w-10 cursor-pointer items-center justify-center"
|
||||
>
|
||||
<StopIcon className="z-10 size-5 text-indigo-500 transition group-hover:text-indigo-400" />
|
||||
<GradientSpinnerBackground
|
||||
className="absolute inset-0 animate-spin"
|
||||
hoverEffect
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
content="Stop generating"
|
||||
/>
|
||||
) : isPreparingAnswer ? (
|
||||
<GradientSpinnerBackground className="flex size-10 min-w-10 items-center justify-center">
|
||||
<Spinner
|
||||
color={{
|
||||
background: "rgba(99, 102, 241, 1)",
|
||||
foreground: "rgba(217, 70, 239, 1)",
|
||||
}}
|
||||
className="size-5"
|
||||
/>
|
||||
</GradientSpinnerBackground>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!message.trim()}
|
||||
LeadingIcon={<ArrowUpIcon className="size-5 text-text-bright" />}
|
||||
variant="primary/large"
|
||||
className="size-10 min-w-10 rounded-full group-disabled/button:border-charcoal-550 group-disabled/button:bg-charcoal-600"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function GradientSpinnerBackground({
|
||||
children,
|
||||
className,
|
||||
hoverEffect = false,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
hoverEffect?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`flex rounded-full bg-gradient-to-br from-indigo-500 via-purple-500 to-fuchsia-500 p-px ${className}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-full w-full items-center justify-center rounded-full bg-charcoal-600 ${
|
||||
hoverEffect ? "transition group-hover:bg-charcoal-550" : ""
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { type ReactNode } from "react";
|
||||
import blurredDashboardBackgroundMenuTop from "~/assets/images/blurred-dashboard-background-menu-top.jpg";
|
||||
import blurredDashboardBackgroundMenuBottom from "~/assets/images/blurred-dashboard-background-menu-bottom.jpg";
|
||||
import blurredDashboardBackgroundTable from "~/assets/images/blurred-dashboard-background-table.jpg";
|
||||
|
||||
export function BackgroundWrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{/* Left menu top background - fixed width 260px, maintains aspect ratio */}
|
||||
<div
|
||||
className="absolute left-0 top-0 w-[260px] bg-contain bg-left-top bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${blurredDashboardBackgroundMenuTop})`,
|
||||
aspectRatio: "auto",
|
||||
height: "100vh",
|
||||
backgroundSize: "260px auto",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Left menu bottom background - fixed width 260px, maintains aspect ratio */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-[260px] bg-contain bg-left-bottom bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${blurredDashboardBackgroundMenuBottom})`,
|
||||
aspectRatio: "auto",
|
||||
height: "100vh",
|
||||
backgroundSize: "260px auto",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Right table background - fixed width 2000px, positioned next to menu */}
|
||||
<div
|
||||
className="absolute top-0 bg-left-top bg-no-repeat"
|
||||
style={{
|
||||
left: "260px",
|
||||
backgroundImage: `url(${blurredDashboardBackgroundTable})`,
|
||||
width: "100%",
|
||||
height: "100vh",
|
||||
backgroundSize: "1200px auto",
|
||||
backgroundColor: "#101214",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content layer */}
|
||||
<div className="relative z-10 h-full w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,26 +5,36 @@ import {
|
||||
ChatBubbleLeftRightIcon,
|
||||
ClockIcon,
|
||||
PlusIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
RectangleGroupIcon,
|
||||
RectangleStackIcon,
|
||||
ServerStackIcon,
|
||||
Squares2X2Icon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useLocation } from "react-use";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
|
||||
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
|
||||
import openBulkActionsPanel from "~/assets/images/open-bulk-actions-panel.png";
|
||||
import selectRunsIndividually from "~/assets/images/select-runs-individually.png";
|
||||
import selectRunsUsingFilters from "~/assets/images/select-runs-using-filters.png";
|
||||
import { useEnvironment } from "~/hooks/useEnvironment";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { type MinimumEnvironment } from "~/presenters/SelectBestEnvironmentPresenter.server";
|
||||
import { NewBranchPanel } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route";
|
||||
import {
|
||||
docsPath,
|
||||
v3BillingPath,
|
||||
v3CreateBulkActionPath,
|
||||
v3EnvironmentPath,
|
||||
v3EnvironmentVariablesPath,
|
||||
v3NewProjectAlertPath,
|
||||
v3NewSchedulePath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { AskAI } from "./AskAI";
|
||||
import { InlineCode } from "./code/InlineCode";
|
||||
import { environmentFullTitle } from "./environments/EnvironmentLabel";
|
||||
import { environmentFullTitle, EnvironmentIcon } from "./environments/EnvironmentLabel";
|
||||
import { Feedback } from "./Feedback";
|
||||
import { EnvironmentSelector } from "./navigation/EnvironmentSelector";
|
||||
import { Button, LinkButton } from "./primitives/Buttons";
|
||||
@@ -33,9 +43,15 @@ import { InfoPanel } from "./primitives/InfoPanel";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { StepNumber } from "./primitives/StepNumber";
|
||||
import { TextLink } from "./primitives/TextLink";
|
||||
import { InitCommandV3, PackageManagerProvider, TriggerDevStepV3 } from "./SetupCommands";
|
||||
import { SimpleTooltip } from "./primitives/Tooltip";
|
||||
import {
|
||||
InitCommandV3,
|
||||
PackageManagerProvider,
|
||||
TriggerDeployStep,
|
||||
TriggerDevStepV3,
|
||||
} from "./SetupCommands";
|
||||
import { StepContentContainer } from "./StepContentContainer";
|
||||
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
|
||||
import { V4Badge } from "./V4Badge";
|
||||
|
||||
export function HasNoTasksDev() {
|
||||
return (
|
||||
@@ -78,26 +94,60 @@ export function HasNoTasksDev() {
|
||||
|
||||
export function HasNoTasksDeployed({ environment }: { environment: MinimumEnvironment }) {
|
||||
return (
|
||||
<InfoPanel
|
||||
title={`You don't have any deployed tasks in ${environmentFullTitle(environment)}`}
|
||||
icon={TaskIcon}
|
||||
iconClassName="text-tasks"
|
||||
panelClassName="max-w-full"
|
||||
accessory={
|
||||
<LinkButton
|
||||
to={docsPath("deployment/overview")}
|
||||
variant="docs/small"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
How to deploy tasks
|
||||
</LinkButton>
|
||||
}
|
||||
>
|
||||
<Paragraph spacing variant="small">
|
||||
Run the <TextLink to={docsPath("deployment/overview")}>CLI deploy command</TextLink> to
|
||||
deploy your tasks to the {environmentFullTitle(environment)} environment.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
<PackageManagerProvider>
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between border-b">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<EnvironmentIcon environment={environment} className="-ml-1 size-8" />
|
||||
<Header1>Deploy your tasks to {environmentFullTitle(environment)}</Header1>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<LinkButton
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
to={docsPath("deployment/overview")}
|
||||
/>
|
||||
}
|
||||
content="Deploy docs"
|
||||
/>
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<LinkButton
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={QuestionMarkCircleIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
to={docsPath("troubleshooting#deployment")}
|
||||
/>
|
||||
}
|
||||
content="Troubleshooting docs"
|
||||
/>
|
||||
<AskAI />
|
||||
</div>
|
||||
</div>
|
||||
<StepNumber stepNumber="1a" title="Run the CLI 'deploy' command" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
This will deploy your tasks to the {environmentFullTitle(environment)} environment. Read
|
||||
the <TextLink to={docsPath("deployment/overview")}>full guide</TextLink>.
|
||||
</Paragraph>
|
||||
<TriggerDeployStep environment={environment} />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="1b" title="Or deploy using GitHub Actions" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Read the <TextLink to={docsPath("github-actions")}>GitHub Actions guide</TextLink> to
|
||||
get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Waiting for tasks to deploy" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh when your tasks are deployed.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</PackageManagerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -431,7 +481,124 @@ export function NoWaitpointTokens() {
|
||||
);
|
||||
}
|
||||
|
||||
function SwitcherPanel() {
|
||||
export function BranchesNoBranchableEnvironment() {
|
||||
const { isManagedCloud } = useFeatures();
|
||||
const organization = useOrganization();
|
||||
|
||||
if (!isManagedCloud) {
|
||||
return (
|
||||
<InfoPanel
|
||||
title="Create a preview environment"
|
||||
icon={BranchEnvironmentIconSmall}
|
||||
iconClassName="text-preview"
|
||||
panelClassName="max-w-full"
|
||||
>
|
||||
<Paragraph spacing variant="small">
|
||||
To add branches you need to have a <InlineCode>RuntimeEnvironment</InlineCode> where{" "}
|
||||
<InlineCode>isBranchableEnvironment</InlineCode> is true. We recommend creating a
|
||||
dedicated one using the "PREVIEW" type.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoPanel
|
||||
title="Upgrade to get preview branches"
|
||||
icon={BranchEnvironmentIconSmall}
|
||||
iconClassName="text-preview"
|
||||
panelClassName="max-w-full"
|
||||
accessory={
|
||||
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
|
||||
Upgrade
|
||||
</LinkButton>
|
||||
}
|
||||
>
|
||||
<Paragraph spacing variant="small">
|
||||
Preview branches in Trigger.dev create isolated environments for testing new features before
|
||||
production.
|
||||
</Paragraph>
|
||||
<Paragraph variant="small">
|
||||
You must be on <V4Badge inline /> to access preview branches. Read our{" "}
|
||||
<TextLink to={docsPath("upgrade-to-v4")}>upgrade to v4 guide</TextLink> to learn more.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function BranchesNoBranches({
|
||||
parentEnvironment,
|
||||
limits,
|
||||
canUpgrade,
|
||||
}: {
|
||||
parentEnvironment: { id: string };
|
||||
limits: { used: number; limit: number };
|
||||
canUpgrade: boolean;
|
||||
}) {
|
||||
const organization = useOrganization();
|
||||
|
||||
if (limits.used >= limits.limit) {
|
||||
return (
|
||||
<InfoPanel
|
||||
title="Upgrade to get preview branches"
|
||||
icon={BranchEnvironmentIconSmall}
|
||||
iconClassName="text-preview"
|
||||
panelClassName="max-w-full"
|
||||
accessory={
|
||||
canUpgrade ? (
|
||||
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
|
||||
Upgrade
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Feedback
|
||||
button={<Button variant="primary/small">Request more</Button>}
|
||||
defaultValue="help"
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Paragraph spacing variant="small">
|
||||
You've reached the limit ({limits.used}/{limits.limit}) of branches for your plan. Upgrade
|
||||
to get branches.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoPanel
|
||||
title="Create your first branch"
|
||||
icon={BranchEnvironmentIconSmall}
|
||||
iconClassName="text-preview"
|
||||
panelClassName="max-w-full"
|
||||
accessory={
|
||||
<NewBranchPanel
|
||||
button={
|
||||
<Button
|
||||
variant="primary/small"
|
||||
LeadingIcon={PlusIcon}
|
||||
leadingIconClassName="text-white"
|
||||
>
|
||||
New branch
|
||||
</Button>
|
||||
}
|
||||
parentEnvironment={parentEnvironment}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Paragraph spacing variant="small">
|
||||
Branches are a way to test new features in isolation before merging them into the main
|
||||
environment.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
Branches are only available when using <V4Badge inline /> or above. Read our{" "}
|
||||
<TextLink to={docsPath("upgrade-to-v4")}>v4 upgrade guide</TextLink> to learn more.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function SwitcherPanel({ title = "Switch to a deployed environment" }: { title?: string }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
@@ -439,7 +606,7 @@ function SwitcherPanel() {
|
||||
return (
|
||||
<div className="flex items-center rounded-md border border-grid-bright bg-background-bright p-3">
|
||||
<Paragraph variant="small" className="grow">
|
||||
Switch to a deployed environment
|
||||
{title}
|
||||
</Paragraph>
|
||||
<EnvironmentSelector
|
||||
organization={organization}
|
||||
@@ -450,3 +617,56 @@ function SwitcherPanel() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BulkActionsNone() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between border-b pb-0.5">
|
||||
<Header1 spacing>Create a bulk action</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
variant="primary/small"
|
||||
LeadingIcon={PlusIcon}
|
||||
to={v3CreateBulkActionPath(organization, project, environment)}
|
||||
>
|
||||
New bulk action
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
<StepNumber stepNumber="1" title="Select runs individually" />
|
||||
<StepContentContainer className="mb-4 flex flex-col gap-4">
|
||||
<Paragraph>Select runs from the runs page individually.</Paragraph>
|
||||
<div>
|
||||
<img src={selectRunsIndividually} alt="Select runs individually" />
|
||||
</div>
|
||||
</StepContentContainer>
|
||||
<div className="mb-5 ml-9 flex items-center gap-2">
|
||||
<div className="h-px w-full bg-grid-bright" />
|
||||
<Paragraph variant="extra-small" className="text-text-dimmed">
|
||||
OR
|
||||
</Paragraph>
|
||||
<div className="h-px w-full bg-grid-bright" />
|
||||
</div>
|
||||
<StepNumber stepNumber="2" title="Select runs using filters" />
|
||||
<StepContentContainer className="flex flex-col gap-4">
|
||||
<Paragraph>
|
||||
Use the filter menu on the runs page to select just the runs you want to bulk action.
|
||||
</Paragraph>
|
||||
<div>
|
||||
<img src={selectRunsUsingFilters} alt="Select runs using filters" />
|
||||
</div>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Open the bulk action panel" />
|
||||
<StepContentContainer className="flex flex-col gap-4">
|
||||
<Paragraph>Click the “Bulk actions” button in the top right of the runs page.</Paragraph>
|
||||
<div>
|
||||
<img src={openBulkActionsPanel} alt="Open the bulk action panel" />
|
||||
</div>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
filterIcon,
|
||||
filterTitle,
|
||||
type TaskRunListSearchFilterKey,
|
||||
type TaskRunListSearchFilters,
|
||||
} from "./runs/v3/RunFilters";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import simplur from "simplur";
|
||||
import { appliedSummary, dateFromString, timeFilterRenderValues } from "./runs/v3/SharedFilters";
|
||||
import { formatNumber } from "~/utils/numberFormatter";
|
||||
import { SpinnerWhite } from "./primitives/Spinner";
|
||||
import { ArrowPathIcon, CheckIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import assertNever from "assert-never";
|
||||
import { AppliedFilter } from "./primitives/AppliedFilter";
|
||||
import { runStatusTitle } from "./runs/v3/TaskRunStatus";
|
||||
import { type TaskRunStatus } from "@trigger.dev/database";
|
||||
|
||||
export const BulkActionMode = z.union([z.literal("selected"), z.literal("filter")]);
|
||||
export type BulkActionMode = z.infer<typeof BulkActionMode>;
|
||||
export const BulkActionAction = z.union([z.literal("cancel"), z.literal("replay")]);
|
||||
export type BulkActionAction = z.infer<typeof BulkActionAction>;
|
||||
|
||||
export function BulkActionFilterSummary({
|
||||
selected,
|
||||
final = false,
|
||||
mode,
|
||||
action,
|
||||
filters,
|
||||
}: {
|
||||
selected?: number;
|
||||
final?: boolean;
|
||||
mode: BulkActionMode;
|
||||
action: BulkActionAction;
|
||||
filters: TaskRunListSearchFilters;
|
||||
}) {
|
||||
switch (mode) {
|
||||
case "selected":
|
||||
return (
|
||||
<Paragraph variant="small">
|
||||
You {!final ? "have " : " "}individually selected {simplur`${selected} run[|s]`} to be{" "}
|
||||
<Action action={action} />.
|
||||
</Paragraph>
|
||||
);
|
||||
case "filter": {
|
||||
const { label, valueLabel, rangeType } = timeFilterRenderValues({
|
||||
from: filters.from ? dateFromString(`${filters.from}`) : undefined,
|
||||
to: filters.to ? dateFromString(`${filters.to}`) : undefined,
|
||||
period: filters.period,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Paragraph variant="small">
|
||||
You {!final ? "have " : " "}selected{" "}
|
||||
<span className="text-text-bright">
|
||||
{final ? selected : <EstimatedCount count={selected} />}
|
||||
</span>{" "}
|
||||
runs to be <Action action={action} /> using these filters:
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-2">
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
label={label}
|
||||
icon={filterIcon("period")}
|
||||
value={valueLabel}
|
||||
removable={false}
|
||||
/>
|
||||
{Object.entries(filters).map(([key, value]) => {
|
||||
if (!value && key !== "period") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typedKey = key as TaskRunListSearchFilterKey;
|
||||
|
||||
switch (typedKey) {
|
||||
case "cursor":
|
||||
case "direction":
|
||||
case "environments":
|
||||
//We need to handle time differently because we have a default
|
||||
case "period":
|
||||
case "from":
|
||||
case "to": {
|
||||
return null;
|
||||
}
|
||||
case "tasks": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values)}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "versions": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values)}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "statuses": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values.map((v) => runStatusTitle(v as TaskRunStatus)))}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "tags": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values)}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "bulkId": {
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={value}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "rootOnly": {
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={"Root only"}
|
||||
icon={filterIcon(key)}
|
||||
value={
|
||||
value ? (
|
||||
<CheckIcon className="size-4" />
|
||||
) : (
|
||||
<XCircleIcon className="size-4" />
|
||||
)
|
||||
}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "runId": {
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={"Run ID"}
|
||||
icon={filterIcon(key)}
|
||||
value={value}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "batchId": {
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={"Batch ID"}
|
||||
icon={filterIcon(key)}
|
||||
value={value}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "scheduleId": {
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={"Schedule ID"}
|
||||
icon={filterIcon(key)}
|
||||
value={value}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "queues": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values.map((v) => v.replace("task/", "")))}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "machines": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values)}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
assertNever(typedKey);
|
||||
}
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Action({ action }: { action: BulkActionAction }) {
|
||||
switch (action) {
|
||||
case "cancel":
|
||||
return (
|
||||
<span>
|
||||
<XCircleIcon className="mb-0.5 inline-block size-4 text-error" />
|
||||
<span className="ml-0.5 text-text-bright">Canceled</span>
|
||||
</span>
|
||||
);
|
||||
case "replay":
|
||||
return (
|
||||
<span>
|
||||
<ArrowPathIcon className="mb-0.5 inline-block size-4 text-blue-400" />
|
||||
<span className="ml-0.5 text-text-bright">Replayed</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function EstimatedCount({ count }: { count?: number }) {
|
||||
if (typeof count === "number") {
|
||||
return <>~{formatNumber(count)}</>;
|
||||
}
|
||||
|
||||
return <SpinnerWhite className="mx-0.5 -mt-0.5 inline size-3" />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function cloudProviderTitle(provider: "aws" | "digitalocean" | (string & {})) {
|
||||
switch (provider) {
|
||||
case "aws":
|
||||
return "Amazon Web Services";
|
||||
case "digitalocean":
|
||||
return "Digital Ocean";
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { GitPullRequestIcon, GitCommitIcon, GitBranchIcon } from "lucide-react";
|
||||
import { type GitMetaLinks } from "~/presenters/v3/BranchesPresenter.server";
|
||||
import { LinkButton } from "./primitives/Buttons";
|
||||
import { SimpleTooltip } from "./primitives/Tooltip";
|
||||
|
||||
export function GitMetadata({ git }: { git?: GitMetaLinks | null }) {
|
||||
if (!git) return null;
|
||||
return (
|
||||
<>
|
||||
{git.pullRequestUrl && git.pullRequestNumber && <GitMetadataPullRequest git={git} />}
|
||||
{git.branchUrl && <GitMetadataBranch git={git} />}
|
||||
{git.shortSha && <GitMetadataCommit git={git} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitMetadataBranch({
|
||||
git,
|
||||
}: {
|
||||
git: Pick<GitMetaLinks, "branchUrl" | "branchName">;
|
||||
}) {
|
||||
return (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<LinkButton
|
||||
variant="minimal/small"
|
||||
LeadingIcon={<GitBranchIcon className="size-4" />}
|
||||
iconSpacing="gap-x-1"
|
||||
to={git.branchUrl}
|
||||
className="pl-1"
|
||||
>
|
||||
{git.branchName}
|
||||
</LinkButton>
|
||||
}
|
||||
content="Jump to GitHub branch"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitMetadataCommit({
|
||||
git,
|
||||
}: {
|
||||
git: Pick<GitMetaLinks, "commitUrl" | "shortSha" | "commitMessage">;
|
||||
}) {
|
||||
return (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<LinkButton
|
||||
variant="minimal/small"
|
||||
to={git.commitUrl}
|
||||
LeadingIcon={<GitCommitIcon className="size-4" />}
|
||||
iconSpacing="gap-x-1"
|
||||
className="pl-1"
|
||||
>
|
||||
{`${git.shortSha} / ${git.commitMessage}`}
|
||||
</LinkButton>
|
||||
}
|
||||
content="Jump to GitHub commit"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitMetadataPullRequest({
|
||||
git,
|
||||
}: {
|
||||
git: Pick<GitMetaLinks, "pullRequestUrl" | "pullRequestNumber" | "pullRequestTitle">;
|
||||
}) {
|
||||
if (!git.pullRequestUrl || !git.pullRequestNumber) return null;
|
||||
|
||||
return (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<LinkButton
|
||||
variant="minimal/small"
|
||||
to={git.pullRequestUrl}
|
||||
LeadingIcon={<GitPullRequestIcon className="size-4" />}
|
||||
iconSpacing="gap-x-1"
|
||||
className="pl-1"
|
||||
>
|
||||
#{git.pullRequestNumber} {git.pullRequestTitle}
|
||||
</LinkButton>
|
||||
}
|
||||
content="Jump to GitHub pull request"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,29 @@
|
||||
import { UserMinusIcon } from "@heroicons/react/20/solid";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./primitives/Tooltip";
|
||||
|
||||
export function ImpersonationBanner() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Form action="/resources/impersonation" method="delete" reloadDocument className="w-full">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={UserMinusIcon}
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
className="text-amber-400"
|
||||
>
|
||||
Stop impersonating
|
||||
</Button>
|
||||
<div>
|
||||
<Form action="/resources/impersonation" method="delete" reloadDocument>
|
||||
<TooltipProvider disableHoverableContent={true}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={UserMinusIcon}
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
className="text-amber-400"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className={"text-xs"}>
|
||||
Stop impersonating
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,10 +15,40 @@ export const DirectionSchema = z.union([z.literal("forward"), z.literal("backwar
|
||||
export type Direction = z.infer<typeof DirectionSchema>;
|
||||
|
||||
export function ListPagination({ list, className }: { list: List; className?: string }) {
|
||||
const bothDisabled = !list.pagination.previous && !list.pagination.next;
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1", className)}>
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<PreviousButton cursor={list.pagination.previous} />
|
||||
<NextButton cursor={list.pagination.next} />
|
||||
<div
|
||||
className={cn(
|
||||
"order-2 h-6 w-px bg-charcoal-600 transition-colors peer-hover/next:bg-charcoal-550 peer-hover/prev:bg-charcoal-550",
|
||||
bothDisabled && "opacity-30"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousButton({ cursor }: { cursor?: string }) {
|
||||
const path = useCursorPath(cursor, "backward");
|
||||
|
||||
return (
|
||||
<div className={cn("peer/prev order-1", !path && "pointer-events-none")}>
|
||||
<LinkButton
|
||||
to={path ?? "#"}
|
||||
variant={"secondary/small"}
|
||||
LeadingIcon={ChevronLeftIcon}
|
||||
className={cn(
|
||||
"flex items-center rounded-r-none border-r-0 pl-2 pr-[0.5625rem]",
|
||||
!path && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
shortcut={{ key: "j" }}
|
||||
tooltip="Previous"
|
||||
disabled={!path}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,45 +57,21 @@ function NextButton({ cursor }: { cursor?: string }) {
|
||||
const path = useCursorPath(cursor, "forward");
|
||||
|
||||
return (
|
||||
<LinkButton
|
||||
to={path ?? "#"}
|
||||
variant={"minimal/small"}
|
||||
TrailingIcon={ChevronRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path && "cursor-not-allowed opacity-50 group-hover/button:bg-transparent"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
shortcut={{ key: "k" }}
|
||||
tooltip="Next"
|
||||
disabled={!path}
|
||||
>
|
||||
Next
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousButton({ cursor }: { cursor?: string }) {
|
||||
const path = useCursorPath(cursor, "backward");
|
||||
|
||||
return (
|
||||
<LinkButton
|
||||
to={path ?? "#"}
|
||||
variant={"minimal/small"}
|
||||
LeadingIcon={ChevronLeftIcon}
|
||||
leadingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path && "cursor-not-allowed opacity-50 group-hover/button:bg-transparent"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
shortcut={{ key: "j" }}
|
||||
tooltip="Previous"
|
||||
disabled={!path}
|
||||
>
|
||||
Prev
|
||||
</LinkButton>
|
||||
<div className={cn("peer/next order-3", !path && "pointer-events-none")}>
|
||||
<LinkButton
|
||||
to={path ?? "#"}
|
||||
variant={"secondary/small"}
|
||||
TrailingIcon={ChevronRightIcon}
|
||||
className={cn(
|
||||
"flex items-center rounded-l-none border-l-0 pl-[0.5625rem] pr-2",
|
||||
!path && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
shortcut={{ key: "k" }}
|
||||
tooltip="Next"
|
||||
disabled={!path}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { MachinePresetName } from "@trigger.dev/core/v3";
|
||||
import { MachineIcon } from "~/assets/icons/MachineIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export const machines = Object.values(MachinePresetName.enum);
|
||||
|
||||
export function MachineLabelCombo({
|
||||
preset,
|
||||
className,
|
||||
iconClassName,
|
||||
labelClassName,
|
||||
}: {
|
||||
preset?: MachinePresetName | null;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
labelClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn("flex items-center gap-1", className)}>
|
||||
<MachineIcon preset={preset ?? undefined} className={cn("size-5", iconClassName)} />
|
||||
<MachineLabel preset={preset} className={labelClassName} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MachineLabel({
|
||||
preset,
|
||||
className,
|
||||
}: {
|
||||
preset?: MachinePresetName | null;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn("text-text-dimmed", className)}>{formatMachinePresetName(preset)}</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function formatMachinePresetName(preset?: MachinePresetName | null): string {
|
||||
if (!preset) {
|
||||
return "No machine yet";
|
||||
}
|
||||
|
||||
switch (preset) {
|
||||
case "micro":
|
||||
return "Micro";
|
||||
case "small-1x":
|
||||
return "Small 1x";
|
||||
case "small-2x":
|
||||
return "Small 2x";
|
||||
case "medium-1x":
|
||||
return "Medium 1x";
|
||||
case "medium-2x":
|
||||
return "Medium 2x";
|
||||
case "large-1x":
|
||||
return "Large 1x";
|
||||
case "large-2x":
|
||||
return "Large 2x";
|
||||
default:
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { MachineIcon } from "~/assets/icons/MachineIcon";
|
||||
import { docsPath } from "~/utils/pathBuilder";
|
||||
import { LinkButton } from "./primitives/Buttons";
|
||||
import { Header3 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export function MachineTooltipInfo() {
|
||||
return (
|
||||
<div className="flex max-w-xs flex-col gap-4 p-1 pb-2">
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-1.5">
|
||||
<MachineIcon preset="no-machine" />
|
||||
<Header3>No machine yet</Header3>
|
||||
</div>
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
The machine is set at the moment the run is dequeued.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-1.5">
|
||||
<MachineIcon preset="micro" />
|
||||
<Header3>Micro</Header3>
|
||||
</div>
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
The smallest and cheapest machine available.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-1.5">
|
||||
<MachineIcon preset="small-1x" /> <Header3>Small 1x & 2x</Header3>
|
||||
</div>
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
Smaller machines for basic workloads. Small 1x is the default machine.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-1.5">
|
||||
<MachineIcon preset="medium-1x" /> <Header3>Medium 1x & 2x</Header3>
|
||||
</div>
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
Medium machines for more demanding workloads.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-1.5">
|
||||
<MachineIcon preset="large-1x" /> <Header3>Large 1x & 2x</Header3>
|
||||
</div>
|
||||
<Paragraph variant="small" className="text-text-dimmed">
|
||||
Larger machines for the most demanding workloads such as video processing. The larger the
|
||||
machine, the more expensive it is.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<LinkButton
|
||||
to={docsPath("machines#machine-configurations")}
|
||||
variant="docs/small"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
Read docs
|
||||
</LinkButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { BunLogoIcon } from "~/assets/icons/BunLogoIcon";
|
||||
import { NodejsLogoIcon } from "~/assets/icons/NodejsLogoIcon";
|
||||
import { parseRuntime, formatRuntimeWithVersion, type NormalizedRuntime } from "~/utils/runtime";
|
||||
|
||||
interface RuntimeIconProps {
|
||||
runtime?: string | null;
|
||||
runtimeVersion?: string | null;
|
||||
className?: string;
|
||||
withLabel?: boolean;
|
||||
}
|
||||
|
||||
const getIcon = (runtime: NormalizedRuntime, className: string) => {
|
||||
switch (runtime) {
|
||||
case "bun":
|
||||
return <BunLogoIcon className={className} />;
|
||||
case "node":
|
||||
return <NodejsLogoIcon className={className} />;
|
||||
default:
|
||||
return <span className="text-text-dimmed">–</span>;
|
||||
}
|
||||
};
|
||||
|
||||
export function RuntimeIcon({
|
||||
runtime,
|
||||
runtimeVersion,
|
||||
className = "h-4 w-4",
|
||||
withLabel = false,
|
||||
}: RuntimeIconProps) {
|
||||
const parsedRuntime = parseRuntime(runtime);
|
||||
|
||||
// Default to Node.js if no runtime is specified
|
||||
const effectiveRuntime = parsedRuntime || {
|
||||
runtime: "node" as const,
|
||||
originalRuntime: "node",
|
||||
displayName: "Node.js",
|
||||
};
|
||||
|
||||
const icon = getIcon(effectiveRuntime.runtime, className);
|
||||
const formattedText = formatRuntimeWithVersion(effectiveRuntime.originalRuntime, runtimeVersion);
|
||||
|
||||
if (withLabel) {
|
||||
return (
|
||||
<span className="flex items-center gap-1">
|
||||
{icon}
|
||||
<span>{formattedText}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof icon === "object" && "type" in icon) {
|
||||
return (
|
||||
<SimpleTooltip button={icon} content={formattedText} side="top" disableHoverableContent />
|
||||
);
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useTriggerCliTag } from "~/hooks/useTriggerCliTag";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsContent,
|
||||
@@ -35,8 +36,6 @@ function usePackageManager() {
|
||||
return context;
|
||||
}
|
||||
|
||||
const v3PackageTag = "latest";
|
||||
|
||||
function getApiUrlArg() {
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
@@ -69,8 +68,9 @@ export function InitCommandV3({ title }: TabsProps) {
|
||||
const project = useProject();
|
||||
const projectRef = project.externalRef;
|
||||
const apiUrlArg = getApiUrlArg();
|
||||
const triggerCliTag = useTriggerCliTag();
|
||||
|
||||
const initCommandParts = [`trigger.dev@${v3PackageTag}`, "init", `-p ${projectRef}`, apiUrlArg];
|
||||
const initCommandParts = [`trigger.dev@${triggerCliTag}`, "init", `-p ${projectRef}`, apiUrlArg];
|
||||
const initCommand = initCommandParts.filter(Boolean).join(" ");
|
||||
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
@@ -118,6 +118,7 @@ export function InitCommandV3({ title }: TabsProps) {
|
||||
}
|
||||
|
||||
export function TriggerDevStepV3({ title }: TabsProps) {
|
||||
const triggerCliTag = useTriggerCliTag();
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
|
||||
return (
|
||||
@@ -139,7 +140,7 @@ export function TriggerDevStepV3({ title }: TabsProps) {
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@${v3PackageTag} dev`}
|
||||
value={`npx trigger.dev@${triggerCliTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
@@ -147,7 +148,7 @@ export function TriggerDevStepV3({ title }: TabsProps) {
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@${v3PackageTag} dev`}
|
||||
value={`pnpm dlx trigger.dev@${triggerCliTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
@@ -155,7 +156,7 @@ export function TriggerDevStepV3({ title }: TabsProps) {
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@${v3PackageTag} dev`}
|
||||
value={`yarn dlx trigger.dev@${triggerCliTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
@@ -163,6 +164,7 @@ export function TriggerDevStepV3({ title }: TabsProps) {
|
||||
}
|
||||
|
||||
export function TriggerLoginStepV3({ title }: TabsProps) {
|
||||
const triggerCliTag = useTriggerCliTag();
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
|
||||
return (
|
||||
@@ -184,7 +186,7 @@ export function TriggerLoginStepV3({ title }: TabsProps) {
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@${v3PackageTag} login`}
|
||||
value={`npx trigger.dev@${triggerCliTag} login`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
@@ -192,7 +194,7 @@ export function TriggerLoginStepV3({ title }: TabsProps) {
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@${v3PackageTag} login`}
|
||||
value={`pnpm dlx trigger.dev@${triggerCliTag} login`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
@@ -200,7 +202,68 @@ export function TriggerLoginStepV3({ title }: TabsProps) {
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@${v3PackageTag} login`}
|
||||
value={`yarn dlx trigger.dev@${triggerCliTag} login`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDeployStep({ title, environment }: TabsProps & { environment: { type: string } }) {
|
||||
const triggerCliTag = useTriggerCliTag();
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
|
||||
// Generate the environment flag based on environment type
|
||||
const getEnvironmentFlag = () => {
|
||||
switch (environment.type) {
|
||||
case "STAGING":
|
||||
return " --env staging";
|
||||
case "PREVIEW":
|
||||
return " --env preview";
|
||||
case "PRODUCTION":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const environmentFlag = getEnvironmentFlag();
|
||||
|
||||
return (
|
||||
<ClientTabs
|
||||
defaultValue="npm"
|
||||
value={activePackageManager}
|
||||
onValueChange={setActivePackageManager}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
{title && <Header3>{title}</Header3>}
|
||||
<ClientTabsList className={title ? "ml-auto" : ""}>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
</div>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@${triggerCliTag} deploy${environmentFlag}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@${triggerCliTag} deploy${environmentFlag}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@${triggerCliTag} deploy${environmentFlag}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
} from "./primitives/SheetV3";
|
||||
import { ShortcutKey } from "./primitives/ShortcutKey";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { useState } from "react";
|
||||
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
|
||||
export function Shortcuts() {
|
||||
return (
|
||||
@@ -23,121 +25,147 @@ export function Shortcuts() {
|
||||
data-action="shortcuts"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
shortcut={{ modifiers: ["shift"], key: "?" }}
|
||||
shortcut={{ modifiers: ["shift"], key: "?", enabled: false }}
|
||||
className="gap-x-0 pl-0.5"
|
||||
iconSpacing="gap-x-0.5"
|
||||
>
|
||||
Shortcuts
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Keyboard className="size-5 text-indigo-500" />
|
||||
<span className="font-sans text-base font-medium text-text-bright">
|
||||
Keyboard shortcuts
|
||||
</span>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
<div className="space-y-6 px-4 pb-4 pt-2">
|
||||
<div className="space-y-3">
|
||||
<Header3>General</Header3>
|
||||
<Shortcut name="Close">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Confirm">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Filter">
|
||||
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Select filter">
|
||||
<ShortcutKey shortcut={{ key: "1" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Previous page">
|
||||
<ShortcutKey shortcut={{ key: "j" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Next page">
|
||||
<ShortcutKey shortcut={{ key: "k" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Help & Feedback">
|
||||
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Runs page</Header3>
|
||||
<Shortcut name="Bulk action: Cancel runs">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Replay runs">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Clear selection">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Run page</Header3>
|
||||
<Shortcut name="Replay run">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Overview">
|
||||
<ShortcutKey shortcut={{ key: "o" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Details">
|
||||
<ShortcutKey shortcut={{ key: "d" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Context">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Metadata">
|
||||
<ShortcutKey shortcut={{ key: "m" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Navigate">
|
||||
<ShortcutKey shortcut={{ key: "arrowup" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowdown" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Expand all">
|
||||
<ShortcutKey shortcut={{ key: "e" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Collapse all">
|
||||
<ShortcutKey shortcut={{ key: "w" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Toggle level">
|
||||
<ShortcutKey shortcut={{ key: "0" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Schedules page</Header3>
|
||||
<Shortcut name="New schedule">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Alerts page</Header3>
|
||||
<Shortcut name="New alert">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
<ShortcutContent />
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutsAutoOpen() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useShortcutKeys({
|
||||
shortcut: { modifiers: ["shift"], key: "?" },
|
||||
action: () => {
|
||||
setIsOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<ShortcutContent />
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function ShortcutContent() {
|
||||
return (
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Keyboard className="size-5 text-indigo-500" />
|
||||
<span className="font-sans text-base font-medium text-text-bright">
|
||||
Keyboard shortcuts
|
||||
</span>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
<div className="space-y-6 px-4 pb-4 pt-2">
|
||||
<div className="space-y-3">
|
||||
<Header3>General</Header3>
|
||||
<Shortcut name="Close">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Confirm">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Ask AI">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"], key: "/" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Filter">
|
||||
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Select filter">
|
||||
<ShortcutKey shortcut={{ key: "1" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Previous page">
|
||||
<ShortcutKey shortcut={{ key: "j" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Next page">
|
||||
<ShortcutKey shortcut={{ key: "k" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Help & Feedback">
|
||||
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Runs page</Header3>
|
||||
<Shortcut name="Bulk action: Cancel runs">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Replay runs">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Clear selection">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Run page</Header3>
|
||||
<Shortcut name="Replay run">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Overview">
|
||||
<ShortcutKey shortcut={{ key: "o" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Details">
|
||||
<ShortcutKey shortcut={{ key: "d" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Context">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Metadata">
|
||||
<ShortcutKey shortcut={{ key: "m" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Navigate">
|
||||
<ShortcutKey shortcut={{ key: "arrowup" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowdown" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Expand all">
|
||||
<ShortcutKey shortcut={{ key: "e" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Collapse all">
|
||||
<ShortcutKey shortcut={{ key: "w" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Toggle level">
|
||||
<ShortcutKey shortcut={{ key: "0" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Schedules page</Header3>
|
||||
<Shortcut name="New schedule">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Alerts page</Header3>
|
||||
<Shortcut name="New alert">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
);
|
||||
}
|
||||
|
||||
function Shortcut({ children, name }: { children: React.ReactNode; name: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Badge } from "./primitives/Badge";
|
||||
import { SimpleTooltip } from "./primitives/Tooltip";
|
||||
|
||||
export function V4Badge({ inline = false, className }: { inline?: boolean; className?: string }) {
|
||||
return (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<Badge variant="extra-small" className={cn(inline ? "inline-grid" : "", className)}>
|
||||
V4
|
||||
</Badge>
|
||||
}
|
||||
content="This feature is only available in V4 and above."
|
||||
disableHoverableContent
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function V4Title({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<span>{children}</span>
|
||||
<V4Badge />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,15 @@ function DebugRunContent({ friendlyId }: { friendlyId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DebugRunData({
|
||||
function DebugRunData(props: UseDataFunctionReturn<typeof loader>) {
|
||||
if (props.engine === "V1") {
|
||||
return <DebugRunDataEngineV1 {...props} />;
|
||||
}
|
||||
|
||||
return <DebugRunDataEngineV2 {...props} />;
|
||||
}
|
||||
|
||||
function DebugRunDataEngineV1({
|
||||
run,
|
||||
queueConcurrencyLimit,
|
||||
queueCurrentConcurrency,
|
||||
@@ -338,3 +346,55 @@ function DebugRunData({
|
||||
</Property.Table>
|
||||
);
|
||||
}
|
||||
|
||||
function DebugRunDataEngineV2({
|
||||
run,
|
||||
queueConcurrencyLimit,
|
||||
queueCurrentConcurrency,
|
||||
envConcurrencyLimit,
|
||||
envCurrentConcurrency,
|
||||
keys,
|
||||
}: UseDataFunctionReturn<typeof loader>) {
|
||||
return (
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>ID</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<ClipboardField value={run.id} variant="tertiary/small" iconButton />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Queue current concurrency</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<span>{queueCurrentConcurrency ?? "0"}</span>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Queue concurrency limit</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<span>{queueConcurrencyLimit ?? "Not set"}</span>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Env current concurrency</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<span>{envCurrentConcurrency ?? "0"}</span>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Env concurrency limit</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<span>{envConcurrencyLimit ?? "Not set"}</span>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
{keys.map((key) => (
|
||||
<Property.Item>
|
||||
<Property.Label>{key.label}</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<ClipboardField value={key.key} variant="tertiary/small" iconButton />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
))}
|
||||
</Property.Table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ export function UpgradePrompt() {
|
||||
<Icon icon={ExclamationCircleIcon} className="h-5 w-5 text-error" />
|
||||
<Paragraph variant="small" className="text-error">
|
||||
You have exceeded the monthly $
|
||||
{(plan.v3Subscription?.plan?.limits.includedUsage ?? 500) / 100} free credits. No runs
|
||||
will execute in Prod until{" "}
|
||||
{(plan.v3Subscription?.plan?.limits.includedUsage ?? 500) / 100} free credits. Existing
|
||||
runs will be queued and new runs won't be created until{" "}
|
||||
<DateTime date={nextMonth} includeTime={false} timeZone="utc" />, or you upgrade.
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,13 @@ import { Clipboard, ClipboardCheck } from "lucide-react";
|
||||
import type { Language, PrismTheme } from "prism-react-renderer";
|
||||
import { Highlight, Prism } from "prism-react-renderer";
|
||||
import { forwardRef, ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import { TextWrapIcon } from "~/assets/icons/TextWrapIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/Dialog";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { TextInlineIcon } from "~/assets/icons/TextInlineIcon";
|
||||
|
||||
//This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx
|
||||
//it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting
|
||||
@@ -31,6 +33,9 @@ type CodeBlockProps = {
|
||||
/** Show copy to clipboard button */
|
||||
showCopyButton?: boolean;
|
||||
|
||||
/** Show text wrapping button */
|
||||
showTextWrapping?: boolean;
|
||||
|
||||
/** Display line numbers */
|
||||
showLineNumbers?: boolean;
|
||||
|
||||
@@ -81,11 +86,17 @@ const defaultTheme: PrismTheme = {
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ["property", "tag", "boolean", "number", "constant", "symbol", "deleted"],
|
||||
types: ["property", "tag", "constant", "symbol", "deleted"],
|
||||
style: {
|
||||
color: "#9B99FF",
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ["boolean", "number"],
|
||||
style: {
|
||||
color: "#E5C07B",
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ["selector", "attr-name", "string", "char", "builtin", "inserted"],
|
||||
style: {
|
||||
@@ -146,12 +157,6 @@ const defaultTheme: PrismTheme = {
|
||||
color: "#F85149",
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ["boolean"],
|
||||
style: {
|
||||
color: "#9B99FF",
|
||||
},
|
||||
},
|
||||
{
|
||||
types: ["char"],
|
||||
style: {
|
||||
@@ -183,6 +188,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
(
|
||||
{
|
||||
showCopyButton = true,
|
||||
showTextWrapping = false,
|
||||
showLineNumbers = true,
|
||||
showOpenInModal = true,
|
||||
highlightedRanges,
|
||||
@@ -202,6 +208,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [modalCopied, setModalCopied] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isWrapped, setIsWrapped] = useState(false);
|
||||
|
||||
const onCopied = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -263,6 +270,25 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
showChrome ? "right-1.5 top-1.5" : "top-2.5"
|
||||
)}
|
||||
>
|
||||
{showTextWrapping && (
|
||||
<TooltipProvider>
|
||||
<Tooltip disableHoverableContent>
|
||||
<TooltipTrigger
|
||||
onClick={() => setIsWrapped(!isWrapped)}
|
||||
className="transition-colors focus-custom hover:cursor-pointer hover:text-text-bright"
|
||||
>
|
||||
{isWrapped ? (
|
||||
<TextInlineIcon className="size-4" />
|
||||
) : (
|
||||
<TextWrapIcon className="size-4" />
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="text-xs">
|
||||
{isWrapped ? "Unwrap" : "Wrap"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{showCopyButton && (
|
||||
<TooltipProvider>
|
||||
<Tooltip open={copied || mouseOver} disableHoverableContent>
|
||||
@@ -311,16 +337,27 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
maxLineWidth={maxLineWidth}
|
||||
className="px-2 py-3"
|
||||
preClassName="text-xs"
|
||||
isWrapped={isWrapped}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
dir="ltr"
|
||||
className="overflow-auto px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
className={cn(
|
||||
"px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
!isWrapped && "overflow-x-auto",
|
||||
isWrapped && "overflow-y-auto"
|
||||
)}
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<pre className="relative mr-2 p-2 font-mono text-xs leading-relaxed" dir="ltr">
|
||||
<pre
|
||||
className={cn(
|
||||
"relative mr-2 p-2 font-mono text-xs leading-relaxed",
|
||||
isWrapped && "[&_span]:whitespace-pre-wrap [&_span]:break-words"
|
||||
)}
|
||||
dir="ltr"
|
||||
>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -355,6 +392,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
maxLineWidth={maxLineWidth}
|
||||
className="min-h-full"
|
||||
preClassName="text-sm"
|
||||
isWrapped={isWrapped}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@@ -410,6 +448,7 @@ type HighlightCodeProps = {
|
||||
maxLineWidth?: number;
|
||||
className?: string;
|
||||
preClassName?: string;
|
||||
isWrapped: boolean;
|
||||
};
|
||||
|
||||
function HighlightCode({
|
||||
@@ -421,11 +460,11 @@ function HighlightCode({
|
||||
maxLineWidth,
|
||||
className,
|
||||
preClassName,
|
||||
isWrapped,
|
||||
}: HighlightCodeProps) {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// This ensures the language definitions are loaded
|
||||
Promise.all([
|
||||
//@ts-ignore
|
||||
import("prismjs/components/prism-json"),
|
||||
@@ -434,16 +473,23 @@ function HighlightCode({
|
||||
]).then(() => setIsLoaded(true));
|
||||
}, []);
|
||||
|
||||
const containerClasses = cn(
|
||||
"px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
!isWrapped && "overflow-x-auto",
|
||||
isWrapped && "overflow-y-auto",
|
||||
className
|
||||
);
|
||||
|
||||
const preClasses = cn(
|
||||
"relative mr-2 font-mono leading-relaxed",
|
||||
preClassName,
|
||||
isWrapped && "[&_span]:whitespace-pre-wrap [&_span]:break-words"
|
||||
);
|
||||
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<div
|
||||
dir="ltr"
|
||||
className={cn(
|
||||
"overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<pre className={cn("relative mr-2 font-mono leading-relaxed", preClassName)}>{code}</pre>
|
||||
<div dir="ltr" className={containerClasses}>
|
||||
<pre className={preClasses}>{code}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -457,22 +503,8 @@ function HighlightCode({
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<div
|
||||
dir="ltr"
|
||||
className={cn(
|
||||
"overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<pre
|
||||
className={cn(
|
||||
"relative mr-2 font-mono leading-relaxed",
|
||||
inheritedClassName,
|
||||
preClassName
|
||||
)}
|
||||
style={inheritedStyle}
|
||||
dir="ltr"
|
||||
>
|
||||
<div dir="ltr" className={containerClasses}>
|
||||
<pre className={cn(preClasses, inheritedClassName)} style={inheritedStyle} dir="ltr">
|
||||
{tokens
|
||||
.map((line, index) => {
|
||||
if (index === tokens.length - 1 && line.length === 1 && line[0].content === "\n") {
|
||||
@@ -495,7 +527,8 @@ function HighlightCode({
|
||||
{...lineProps}
|
||||
className={cn(
|
||||
"flex w-full justify-start transition-opacity duration-500",
|
||||
lineProps.className
|
||||
lineProps.className,
|
||||
isWrapped && "flex-wrap"
|
||||
)}
|
||||
style={{
|
||||
opacity: shouldDim ? dimAmount : undefined,
|
||||
@@ -504,9 +537,10 @@ function HighlightCode({
|
||||
>
|
||||
{showLineNumbers && (
|
||||
<div
|
||||
className={
|
||||
"mr-2 flex-none select-none text-right text-charcoal-500 transition-opacity duration-500"
|
||||
}
|
||||
className={cn(
|
||||
"mr-2 flex-none select-none text-right text-charcoal-500 transition-opacity duration-500",
|
||||
isWrapped && "sticky left-0"
|
||||
)}
|
||||
style={{
|
||||
width: `calc(8 * ${(maxLineWidth as number) / 16}rem)`,
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json as jsonLang } from "@codemirror/lang-json";
|
||||
import type { ViewUpdate } from "@codemirror/view";
|
||||
import { json as jsonLang, jsonParseLinter } from "@codemirror/lang-json";
|
||||
import type { EditorView, ViewUpdate } from "@codemirror/view";
|
||||
import { CheckIcon, ClipboardIcon, TrashIcon } from "@heroicons/react/20/solid";
|
||||
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
|
||||
import { useCodeMirror } from "@uiw/react-codemirror";
|
||||
@@ -8,6 +8,7 @@ import { cn } from "~/utils/cn";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { getEditorSetup } from "./codeMirrorSetup";
|
||||
import { darkTheme } from "./codeMirrorTheme";
|
||||
import { linter, lintGutter, type Diagnostic } from "@codemirror/lint";
|
||||
|
||||
export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
defaultValue?: string;
|
||||
@@ -18,18 +19,36 @@ export interface JSONEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
|
||||
onBlur?: (code: string) => void;
|
||||
showCopyButton?: boolean;
|
||||
showClearButton?: boolean;
|
||||
linterEnabled?: boolean;
|
||||
allowEmpty?: boolean;
|
||||
additionalActions?: React.ReactNode;
|
||||
}
|
||||
|
||||
const languages = {
|
||||
json: jsonLang,
|
||||
};
|
||||
|
||||
function emptyAwareJsonLinter() {
|
||||
return (view: EditorView): Diagnostic[] => {
|
||||
const content = view.state.doc.toString().trim();
|
||||
|
||||
// return no errors if content is empty
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return jsonParseLinter()(view);
|
||||
};
|
||||
}
|
||||
|
||||
type JSONEditorDefaultProps = Partial<JSONEditorProps>;
|
||||
|
||||
const defaultProps: JSONEditorDefaultProps = {
|
||||
language: "json",
|
||||
readOnly: true,
|
||||
basicSetup: false,
|
||||
linterEnabled: true,
|
||||
allowEmpty: true,
|
||||
};
|
||||
|
||||
export function JSONEditor(opts: JSONEditorProps) {
|
||||
@@ -44,6 +63,9 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
autoFocus,
|
||||
showCopyButton = true,
|
||||
showClearButton = true,
|
||||
linterEnabled,
|
||||
allowEmpty,
|
||||
additionalActions,
|
||||
} = {
|
||||
...defaultProps,
|
||||
...opts,
|
||||
@@ -56,6 +78,19 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
|
||||
extensions.push(languageExtension());
|
||||
|
||||
if (linterEnabled) {
|
||||
extensions.push(lintGutter());
|
||||
|
||||
switch (language) {
|
||||
case "json": {
|
||||
extensions.push(allowEmpty ? linter(emptyAwareJsonLinter()) : linter(jsonParseLinter()));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
language satisfies never;
|
||||
}
|
||||
}
|
||||
|
||||
const editor = useRef<HTMLDivElement>(null);
|
||||
const settings: Omit<UseCodeMirror, "onBlur"> = {
|
||||
...opts,
|
||||
@@ -90,13 +125,13 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
}
|
||||
}, [defaultValue, view]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
const clear = () => {
|
||||
if (view === undefined) return;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: undefined },
|
||||
});
|
||||
onChange?.("");
|
||||
}, [view]);
|
||||
};
|
||||
|
||||
const copy = useCallback(() => {
|
||||
if (view === undefined) return;
|
||||
@@ -119,6 +154,7 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
>
|
||||
{showButtons && (
|
||||
<div className="mx-3 flex items-center justify-end gap-2 border-b border-grid-dimmed">
|
||||
{additionalActions && additionalActions}
|
||||
{showClearButton && (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user