Compare commits
326 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f9f25481e | |||
| 67aaffb6fc | |||
| e3cf456c69 | |||
| bf7827e7b8 | |||
| bc020a3ffe | |||
| b361afbfe4 | |||
| a3d809740d | |||
| f93eae300e | |||
| a2365e406d | |||
| 42d319c2d1 | |||
| b66d5525ef | |||
| 719c0a0b94 | |||
| f1c768a255 | |||
| d9c9e80bc4 | |||
| d39932ebf7 | |||
| 9bcb8cb42a | |||
| 2374f8e8ac | |||
| a22b5869e4 | |||
| f1571cbfab | |||
| 222dfed4d2 | |||
| 89c0e68fb5 | |||
| 5b745dc1a4 | |||
| 2dea8dec35 | |||
| 252f5b3967 | |||
| 014d233339 | |||
| e398bdf859 | |||
| 426316aa3c | |||
| 4f9b8d6721 | |||
| a345ad2d57 | |||
| e2b67400ea | |||
| c4dc2849af | |||
| a8263dec86 | |||
| 53054d0146 | |||
| 74236ae7c1 | |||
| 055d468b38 | |||
| fc473105e3 | |||
| 3a35d60091 | |||
| f11c0f991b | |||
| 1cf181fd02 | |||
| 412d0149d5 | |||
| d614c8d2ca | |||
| 4329d1130a | |||
| 17f9fc8273 | |||
| db611bebba | |||
| 215b60c742 | |||
| 9c89ab40aa | |||
| b342e06583 | |||
| 4418de515b | |||
| 0d2a71c5cf | |||
| 9e02b45808 | |||
| 395abe1b92 | |||
| b35eebb666 | |||
| 9ecf07731a | |||
| b890a64046 | |||
| 4266b308e7 | |||
| 67c62f7d25 | |||
| c6d6a9191c | |||
| cd8f6b9af0 | |||
| a49b659701 | |||
| 61c9a7c7d6 | |||
| de302858f8 | |||
| cf9d466b9e | |||
| 7b3ffe040b | |||
| 737ff9928c | |||
| fc4b95d69e | |||
| 6c546936f3 | |||
| 08e4fc002f | |||
| 0e347b001b | |||
| 2e56af9742 | |||
| dc5eb68a0b | |||
| 891278ab1f | |||
| 41e1ac9e0b | |||
| c5aaabdb4e | |||
| affa3f25ee | |||
| 86c24afb44 | |||
| 6afda40c35 | |||
| b585d1f2c5 | |||
| f26ba9a5cd | |||
| ba0ffacb15 | |||
| e55ca0279b | |||
| 9304c447be | |||
| ebec14093d | |||
| 67cac97b26 | |||
| 0f32dee0eb | |||
| 479d355b7b | |||
| 49e9dd8cb2 | |||
| cae3298564 | |||
| 1f190fe680 | |||
| 85ec1af198 | |||
| 52c364d691 | |||
| a5d83971aa | |||
| 5b9c67aeef | |||
| 1c98eb2b61 | |||
| f424c81086 | |||
| 0a33bf7206 | |||
| 819b663ad7 | |||
| 478ce006cb | |||
| fe9435f63f | |||
| d7cc2c9d6c | |||
| 851b1769cb | |||
| e8381d5341 | |||
| 20aa1cbe9b | |||
| 883feab257 | |||
| 83304f86f8 | |||
| 5d78bba5e3 | |||
| e87a0cba1d | |||
| 8852d5b388 | |||
| 79ea8edc7c | |||
| 553370a9e5 | |||
| 47c18d9146 | |||
| c385db63be | |||
| 86b2674e68 | |||
| 161b2b3323 | |||
| 9b6f8f9238 | |||
| f50cee66d9 | |||
| 44d443a048 | |||
| 2a1273b683 | |||
| 64401ee8ec | |||
| b7845685eb | |||
| 52c9d485f8 | |||
| ca47e6bdcd | |||
| 933cf0f119 | |||
| b6517af522 | |||
| 479445f741 | |||
| 1fabcf68fa | |||
| d7c79ee651 | |||
| 8d6e710dc2 | |||
| 90a302f9f7 | |||
| 07efef405d | |||
| dd63fe6e9f | |||
| d8c645a3f0 | |||
| 24600d2c01 | |||
| 584936fa29 | |||
| a54bdb23bf | |||
| 07f7c65054 | |||
| d04c8e89f6 | |||
| b69902dd84 | |||
| 2aabb994cf | |||
| 310d51bac0 | |||
| 6be1099a12 | |||
| baf3a84cda | |||
| 70f9bd0d70 | |||
| a1860dbaae | |||
| d69e4e712d | |||
| 7fae67c47d | |||
| 9ae0ca64af | |||
| da69fa0613 | |||
| 336029b842 | |||
| 29edcd3df9 | |||
| a9ff32418e | |||
| 7358fcb891 | |||
| 28052daad9 | |||
| 364c8c5f7f | |||
| 4b3b418abb | |||
| 2e354d342c | |||
| dd879c8e4a | |||
| af485b9180 | |||
| a739ebaa88 | |||
| 3ebc2578e0 | |||
| 0b657b33f9 | |||
| 7ac942dc0e | |||
| 9b12016428 | |||
| d6b44de4ba | |||
| 17df4839d7 | |||
| e3f78178f7 | |||
| da90ee13c3 | |||
| 583da458ec | |||
| dcf95c4eb2 | |||
| 32cf5790cb | |||
| c272e38de2 | |||
| ca78ddc2c2 | |||
| 07ed8c346a | |||
| d4391f2e2d | |||
| 2c328ba93d | |||
| 68dbc07ee8 | |||
| 58448243af | |||
| 6c4047cf21 | |||
| 831860eace | |||
| db46f2a69a | |||
| 4dd6cf18dc | |||
| adf66d23b8 | |||
| 5c42831a60 | |||
| 26f5e7774d | |||
| f209a3b364 | |||
| a93b554f8b | |||
| 0f342cd1be | |||
| 7df2f85a1f | |||
| f14180d13c | |||
| ab6b9514cd | |||
| 98345d67d9 | |||
| 38f5a90399 | |||
| 1b2635ae4a | |||
| 129f023d11 | |||
| 795e637dec | |||
| 5238c424fc | |||
| 1bbd7e6dc3 | |||
| 3bb82ed9a5 | |||
| ff4ff869ab | |||
| 1dcee2b338 | |||
| 0a798446c8 | |||
| 209942a63d | |||
| 57a9b35870 | |||
| a16b65f666 | |||
| 5871745a92 | |||
| 87b5dfbf11 | |||
| 5af2003516 | |||
| 85ce729bf2 | |||
| 5701d1da42 | |||
| bc61d83764 | |||
| 5bf125be0d | |||
| babe1c0e54 | |||
| 1224fceb18 | |||
| 8277f4d249 | |||
| 73cb8839a5 | |||
| 5d0a731cf6 | |||
| 25a152517e | |||
| d1092fcd2c | |||
| 61d33ed1b8 | |||
| 7a51612fc8 | |||
| 4d380173c8 | |||
| 8880d8afe2 | |||
| ef037af457 | |||
| 390ac6101e | |||
| aa97bf4a52 | |||
| 86d6e102a4 | |||
| adf497bc3e | |||
| f7bf25f03f | |||
| f8cab96fa2 | |||
| 225614fea3 | |||
| 740b7b2385 | |||
| 26d223ab36 | |||
| 0c4fd9a631 | |||
| 5060eec01b | |||
| aaea064208 | |||
| 9346e4358d | |||
| 3aafd0480b | |||
| 771feb5395 | |||
| 03703af682 | |||
| 5359af1242 | |||
| dfa47b6201 | |||
| c01138b56e | |||
| 70fdedcea5 | |||
| 49ca4c4430 | |||
| 812b5afe6f | |||
| 922520cefb | |||
| 4bd1786ae5 | |||
| daae6df428 | |||
| fc31c6c07a | |||
| 6d4676f204 | |||
| 0a51ef3a06 | |||
| 772f1b41a0 | |||
| d42ba80108 | |||
| 6a3c563f14 | |||
| c04cdfde8c | |||
| 750c1ff1e3 | |||
| 0f9f010206 | |||
| 11f8ff1bb4 | |||
| 85b3352764 | |||
| 9fb8dceef4 | |||
| 0c14e4cdfe | |||
| 2099c6308e | |||
| 08e6cad28a | |||
| f3efcc0c28 | |||
| 37ef335b66 | |||
| 33d555d00e | |||
| 17f6f29d05 | |||
| 08f7c639ef | |||
| 1567239718 | |||
| de652c1dfb | |||
| 1f3733b70f | |||
| b5aea6c534 | |||
| 0769dc4315 | |||
| 5d00fc7cdb | |||
| 00b0c3e02e | |||
| 7e3a82ef47 | |||
| 5dda6cd16c | |||
| 76b7fb2337 | |||
| 68cbfd8d23 | |||
| 41a49f6bb2 | |||
| 2f755158b4 | |||
| 419b93809d | |||
| bd4bc51daa | |||
| ff540c9e4a | |||
| 3f217ff3e0 | |||
| 9cb39bf7d7 | |||
| ca05d5f603 | |||
| 4dc46cbbe4 | |||
| 1dcd87a2aa | |||
| c4cb98af5c | |||
| 6ebd435e81 | |||
| caf203c084 | |||
| dbc2e3f713 | |||
| a5b49cb61f | |||
| 096151c014 | |||
| 5d28f1a3d6 | |||
| 56ae0ed0d3 | |||
| a2f5dc8bc6 | |||
| d56b2eded5 | |||
| 0ae9adafbf | |||
| 03403ad9cb | |||
| 067e19fec9 | |||
| 2cce68b5f7 | |||
| e3c3aa91e1 | |||
| 5c0ca83852 | |||
| 2b24104c97 | |||
| 5977a5aa51 | |||
| 131d0dbea4 | |||
| 59277731c4 | |||
| 742d16087b | |||
| 0ab51d62bc | |||
| 756024da78 | |||
| c47ad5e038 | |||
| 888344797e | |||
| 4a7caa68b6 | |||
| 3bc54d36f7 | |||
| 5295a3f798 | |||
| 14429b8b66 | |||
| 5dd1bcc589 | |||
| ed5ee16de5 | |||
| 076185d19f | |||
| 1e07964d81 | |||
| f406e59c45 | |||
| 75550f535c | |||
| 2b48e6d04b | |||
| fe14947bb0 | |||
| 0afa119721 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Vastly improved dev command output
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
add machine config and secure zod connection
|
||||
@@ -14,7 +14,12 @@
|
||||
"ignore": [
|
||||
"webapp",
|
||||
"emails",
|
||||
"@trigger.dev/database"
|
||||
"proxy",
|
||||
"yalt",
|
||||
"@trigger.dev/database",
|
||||
"coordinator",
|
||||
"docker-provider",
|
||||
"kubernetes-provider"
|
||||
],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/nestjs": patch
|
||||
---
|
||||
|
||||
fix: [nestjs integration] fastify HTTP adapter detection now works correctly for response headers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Changed "Worker" to "Version" in the dev command key
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Handle string and non-stringifiable outputs like functions
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "beta",
|
||||
"initialVersions": {
|
||||
"coordinator": "0.0.1",
|
||||
"docker-provider": "0.0.1",
|
||||
"kubernetes-provider": "0.0.1",
|
||||
"proxy": "0.0.11",
|
||||
"webapp": "1.0.0",
|
||||
"yalt": "0.0.1",
|
||||
"@trigger.dev/airtable": "2.3.18",
|
||||
"@trigger.dev/github": "2.3.18",
|
||||
"@trigger.dev/linear": "2.3.18",
|
||||
"@trigger.dev/openai": "2.3.18",
|
||||
"@trigger.dev/plain": "2.3.18",
|
||||
"@trigger.dev/replicate": "2.3.18",
|
||||
"@trigger.dev/resend": "2.3.18",
|
||||
"@trigger.dev/sendgrid": "2.3.18",
|
||||
"@trigger.dev/shopify": "2.3.18",
|
||||
"@trigger.dev/slack": "2.3.18",
|
||||
"@trigger.dev/stripe": "2.3.18",
|
||||
"@trigger.dev/supabase": "2.3.18",
|
||||
"@trigger.dev/typeform": "2.3.18",
|
||||
"@trigger.dev/astro": "2.3.18",
|
||||
"@trigger.dev/cli": "2.3.18",
|
||||
"trigger.dev": "2.3.18",
|
||||
"@trigger.dev/core": "2.3.18",
|
||||
"@trigger.dev/core-apps": "0.0.0",
|
||||
"@trigger.dev/core-backend": "2.3.18",
|
||||
"@trigger.dev/database": "0.0.1",
|
||||
"emails": "1.0.0",
|
||||
"@trigger.dev/eslint-plugin": "2.3.18",
|
||||
"@trigger.dev/express": "2.3.18",
|
||||
"@trigger.dev/hono": "2.3.18",
|
||||
"@trigger.dev/integration-kit": "2.3.18",
|
||||
"@trigger.dev/nestjs": "2.3.18",
|
||||
"@trigger.dev/nextjs": "2.3.18",
|
||||
"@trigger.dev/otlp-importer": "2.3.11",
|
||||
"@trigger.dev/react": "2.3.18",
|
||||
"@trigger.dev/remix": "2.3.18",
|
||||
"@trigger.dev/sveltekit": "2.3.18",
|
||||
"@trigger.dev/testing": "2.3.18",
|
||||
"@trigger.dev/sdk": "2.3.18",
|
||||
"@trigger.dev/yalt": "2.3.18"
|
||||
},
|
||||
"changesets": [
|
||||
"chilled-hornets-move",
|
||||
"clean-pianos-listen",
|
||||
"lemon-jobs-repair",
|
||||
"light-bulldogs-press",
|
||||
"many-ligers-pump",
|
||||
"real-planets-stare",
|
||||
"strange-ghosts-matter",
|
||||
"stupid-bulldogs-applaud",
|
||||
"sweet-lizards-press",
|
||||
"tall-bees-wave"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Fixed incorrect span timings around checkpoints by implementing a precise wall clock that resets after restores
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Added DEBUG to the ignored env vars
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fixed an issue where the trigger.dev package was not being built before publishing to npm
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"trigger.dev": major
|
||||
"@trigger.dev/core": major
|
||||
"@trigger.dev/otlp-importer": major
|
||||
"@trigger.dev/sdk": major
|
||||
---
|
||||
|
||||
Updates to support Trigger.dev v3
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Dynamically import superjson and fix some bundling issues
|
||||
+36
-1
@@ -12,6 +12,11 @@ APP_ENV=development
|
||||
APP_ORIGIN=http://localhost:3030
|
||||
NODE_ENV=development
|
||||
|
||||
# Redis is used for concurrency control
|
||||
# REDIS_HOST="localhost"
|
||||
# REDIS_PORT="6379"
|
||||
# REDIS_TLS_DISABLED="true"
|
||||
|
||||
# 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"
|
||||
@@ -26,6 +31,9 @@ NODE_ENV=development
|
||||
# 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=
|
||||
@@ -37,4 +45,31 @@ CLOUD_LINEAR_CLIENT_ID=
|
||||
CLOUD_LINEAR_CLIENT_SECRET=
|
||||
CLOUD_SLACK_APP_HOST=
|
||||
CLOUD_SLACK_CLIENT_ID=
|
||||
CLOUD_SLACK_CLIENT_SECRET=
|
||||
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_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"
|
||||
# OBJECT_STORE_ACCESS_KEY_ID=
|
||||
# OBJECT_STORE_SECRET_ACCESS_KEY=
|
||||
# RUNTIME_WAIT_THRESHOLD_IN_MS=10000
|
||||
|
||||
# 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,
|
||||
@@ -0,0 +1,21 @@
|
||||
name: OpenTelemetry Auto-Instrumentation Request
|
||||
description: Suggest an SDK that you'd like to be auto-instrumented in the Run log view
|
||||
title: "auto-instrumentation: "
|
||||
labels: ["🌟 enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What API or SDK would you to have automatic spans for?
|
||||
description: A clear description of which API or SDK you'd like, and links to it.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is there an existing OpenTelemetry auto-instrumentation package?
|
||||
description: You can search for existing ones – https://opentelemetry.io/ecosystem/registry/?component=instrumentation&language=js
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
|
||||
@@ -4,30 +4,36 @@ on:
|
||||
jobs:
|
||||
e2e:
|
||||
name: "🧪 E2E Tests"
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME || vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN || vars.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -42,7 +48,10 @@ jobs:
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
cd apps/webapp && pnpm run build:server
|
||||
cd ../..
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
pnpm --filter @trigger.dev/otlp-importer generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
name: 🤖 PR Checks
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "**.mdx"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: "🐳 Publish Docker"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
short_sha: ${{ steps.get_commit.outputs.sha_short }}
|
||||
steps:
|
||||
- name: Setup Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: 🆚 Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
if [[ $IMAGE_TAG == v.docker.* ]]; then
|
||||
ORIGINAL_VERSION="${IMAGE_TAG#v.docker.}"
|
||||
IMAGE_TAG="v${ORIGINAL_VERSION}"
|
||||
elif [[ $IMAGE_TAG == build-* ]]; then
|
||||
IMAGE_TAG="${IMAGE_TAG#build-}"
|
||||
fi
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
# Handle main branch specifically
|
||||
IMAGE_TAG="main"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
else
|
||||
echo "Invalid reference: ${GITHUB_REF}"
|
||||
exit 1
|
||||
fi
|
||||
echo "::set-output name=version::${IMAGE_TAG}"
|
||||
- name: 🔢 Get the commit hash
|
||||
id: get_commit
|
||||
run: |
|
||||
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐳 Build image and push to GitHub Container Registry
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/triggerdotdev/trigger.dev:${{ steps.get_version.outputs.version }}
|
||||
push: true
|
||||
@@ -0,0 +1,94 @@
|
||||
name: "🚢 Publish Infra Images"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infra-dev-*"
|
||||
- "infra-test-*"
|
||||
- "infra-prod-*"
|
||||
paths:
|
||||
- ".github/workflows/publish.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- "!packages/**/*.eslintrc"
|
||||
- "apps/**"
|
||||
- "!apps/**/*.md"
|
||||
- "!apps/**/*.eslintrc"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "!integrations/**/*.eslintrc"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
- "docker/Dockerfile"
|
||||
- "docker/scripts/**"
|
||||
- "tests/**"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
env:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
package: [coordinator, kubernetes-provider]
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate image reference
|
||||
id: prep
|
||||
# WARNING: This step expects the workflow to have been triggered by a specific tag format of: infra-${env}-*
|
||||
run: |
|
||||
env=$(echo ${{ github.ref_name }} | cut -d- -f2)
|
||||
sha=${GITHUB_SHA::7}
|
||||
ts=$(date +%s)
|
||||
if [[ "${{ matrix.package }}" == *-provider ]]; then
|
||||
provider_type=$(echo ${{ matrix.package }} | cut -d- -f1)
|
||||
repository=provider/${provider_type}
|
||||
else
|
||||
repository=${{ matrix.package }}
|
||||
fi
|
||||
echo "IMAGE_TAG=${env}-${sha}-${ts}" >> "$GITHUB_OUTPUT"
|
||||
echo "REPOSITORY=${repository}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- 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
|
||||
run: |
|
||||
docker build -t infra_image -f ./apps/${{ matrix.package }}/Containerfile .
|
||||
|
||||
# ..to push image
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
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.prep.outputs.REPOSITORY }}
|
||||
IMAGE_TAG: ${{ steps.prep.outputs.IMAGE_TAG }}
|
||||
@@ -4,11 +4,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- improvements/*
|
||||
tags:
|
||||
- "v.docker.*"
|
||||
- "build-*"
|
||||
paths:
|
||||
- ".github/workflows/publish.yml"
|
||||
- ".github/workflows/typecheck.yml"
|
||||
- ".github/workflows/unit-tests.yml"
|
||||
- ".github/workflows/e2e.yml"
|
||||
- ".github/workflows/publish-docker.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- "!packages/**/*.eslintrc"
|
||||
@@ -51,71 +55,5 @@ jobs:
|
||||
|
||||
publish:
|
||||
needs: [typecheck, units, e2e]
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
short_sha: ${{ steps.get_commit.outputs.sha_short }}
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🆚 Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
if [[ $IMAGE_TAG == v.docker.* ]]; then
|
||||
ORIGINAL_VERSION="${IMAGE_TAG#v.docker.}"
|
||||
IMAGE_TAG="v${ORIGINAL_VERSION}"
|
||||
fi
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
elif [[ $GITHUB_REF == refs/heads/improvements/* ]]; then
|
||||
ORIGINAL_VERSION="${GITHUB_REF#refs/heads/improvements/}"
|
||||
IMAGE_TAG="${ORIGINAL_VERSION}.rc"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
elif [[ $GITHUB_REF == refs/heads/* ]]; then
|
||||
IMAGE_TAG="${GITHUB_REF#refs/heads/}"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
else
|
||||
echo "Invalid reference: ${GITHUB_REF}"
|
||||
exit 1
|
||||
fi
|
||||
echo "::set-output name=version::${IMAGE_TAG}"
|
||||
- name: 🔢 Get the commit hash
|
||||
id: get_commit
|
||||
run: |
|
||||
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
- name: 🐳 Build Docker Image
|
||||
run: |
|
||||
docker build -t release_build_image -f ./docker/Dockerfile .
|
||||
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐙 Push to GitHub Container Registry
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
- name: 🐙 Push 'latest' to GitHub Container Registry
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
|
||||
docker push $REGISTRY/$REPOSITORY:latest
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -8,12 +8,11 @@ on:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: 🦋 Changesets Release
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
if: |
|
||||
github.repository == 'triggerdotdev/trigger.dev'
|
||||
outputs:
|
||||
@@ -31,14 +30,19 @@ jobs:
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -46,7 +50,7 @@ jobs:
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck
|
||||
run: pnpm run typecheck --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
|
||||
- name: 🔐 Setup npm auth
|
||||
run: |
|
||||
|
||||
@@ -3,7 +3,7 @@ on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
@@ -14,14 +14,19 @@ jobs:
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests"
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -14,14 +14,29 @@ jobs:
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: ⎔ Setup Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: ⎔ Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: "1.0.15"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
||||
+2
-1
@@ -53,4 +53,5 @@ apps/**/public/build
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
.cosine
|
||||
.cosine
|
||||
.trigger/
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "packages/otlp-importer/protos"]
|
||||
path = packages/otlp-importer/protos
|
||||
url = https://github.com/open-telemetry/opentelemetry-proto.git
|
||||
Vendored
+25
-1
@@ -9,7 +9,7 @@
|
||||
"request": "launch",
|
||||
"name": "Debug WebApp",
|
||||
"command": "pnpm run dev --filter webapp",
|
||||
"envFile": "${workspaceFolder}/apps/webapp/.env",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"sourceMaps": true
|
||||
},
|
||||
@@ -28,6 +28,30 @@
|
||||
"envFile": "${workspaceFolder}/references/job-catalog/.env",
|
||||
"cwd": "${workspaceFolder}/references/job-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 Dev CLI",
|
||||
"command": "pnpm exec trigger.dev dev --log-level debug",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 Deploy CLI",
|
||||
"command": "pnpm exec trigger.dev deploy",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Trigger.dev CLI (v3)",
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"deno.enablePaths": ["references/deno-reference"],
|
||||
"deno.enablePaths": ["references/deno-reference", "runtime_tests/tests/deno"],
|
||||
"debug.toolBarLocation": "commandCenter"
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ pnpm exec changeset version --snapshot prerelease
|
||||
3. Build the packages:
|
||||
|
||||
```sh
|
||||
pnpm run build --filter "@trigger.dev/*"
|
||||
pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
```
|
||||
|
||||
4. Publish the snapshot (replace "dev" with your tag)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
HTTP_SERVER_PORT=8020
|
||||
PLATFORM_ENABLED=true
|
||||
PLATFORM_WS_PORT=3030
|
||||
SECURE_CONNECTION=false
|
||||
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.env
|
||||
@@ -0,0 +1,60 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
FROM node:18-bullseye-slim@sha256:a4edd54dcfdcacc8a4100fee71498e8671d99556a1acf5614539214a70092426 AS node-18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM node-18 AS pruner
|
||||
|
||||
COPY --chown=node:node . .
|
||||
RUN npx -q turbo@1.10.9 prune --scope=coordinator --docker
|
||||
RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
||||
|
||||
FROM node-18 AS base
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y buildah ca-certificates dumb-init \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --chown=node:node .gitignore .gitignore
|
||||
COPY --from=pruner --chown=node:node /app/out/json/ .
|
||||
COPY --from=pruner --chown=node:node /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
COPY --from=pruner --chown=node:node /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||
|
||||
FROM base AS dev-deps
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV development
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch --no-frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --ignore-scripts --no-frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
RUN corepack enable
|
||||
|
||||
COPY --from=pruner --chown=node:node /app/out/full/ .
|
||||
COPY --from=dev-deps --chown=node:node /app/ .
|
||||
COPY --chown=node:node turbo.json turbo.json
|
||||
|
||||
RUN pnpm run -r --filter coordinator build:bundle
|
||||
|
||||
FROM alpine AS cri-tools
|
||||
|
||||
WORKDIR /cri-tools
|
||||
|
||||
ARG CRICTL_VERSION=v1.29.0
|
||||
ARG CRICTL_CHECKSUM=sha256:d16a1ffb3938f5a19d5c8f45d363bd091ef89c0bc4d44ad16b933eede32fdcbb
|
||||
ADD --checksum=${CRICTL_CHECKSUM} \
|
||||
https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRICTL_VERSION}/crictl-${CRICTL_VERSION}-linux-amd64.tar.gz .
|
||||
RUN tar zxvf crictl-${CRICTL_VERSION}-linux-amd64.tar.gz
|
||||
|
||||
FROM base AS runner
|
||||
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV production
|
||||
|
||||
COPY --from=cri-tools --chown=node:node /cri-tools/crictl /usr/local/bin
|
||||
COPY --from=builder --chown=node:node /app/apps/coordinator/dist/index.mjs ./index.mjs
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "/usr/bin/dumb-init", "--", "/usr/local/bin/node", "./index.mjs" ]
|
||||
@@ -0,0 +1,3 @@
|
||||
# Coordinator
|
||||
|
||||
Sits between the platform and tasks. Facilitates communication and checkpointing, amongst other things.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "coordinator",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.cjs",
|
||||
"scripts": {
|
||||
"build": "npm run build:bundle",
|
||||
"build:bundle": "esbuild src/index.ts --bundle --outfile=dist/index.mjs --platform=node --format=esm --target=esnext --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);\"",
|
||||
"build:image": "docker build -f Containerfile . -t coordinator",
|
||||
"dev": "tsx --no-warnings=ExperimentalWarning --require dotenv/config --watch src/index.ts",
|
||||
"start": "tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"execa": "^8.0.1",
|
||||
"nanoid": "^5.0.6",
|
||||
"prom-client": "^15.1.0",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18",
|
||||
"dotenv": "^16.4.2",
|
||||
"esbuild": "^0.19.11",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
import { createServer } from "node:http";
|
||||
import { $ } from "execa";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Server } from "socket.io";
|
||||
import {
|
||||
CoordinatorToPlatformMessages,
|
||||
CoordinatorToProdWorkerMessages,
|
||||
PlatformToCoordinatorMessages,
|
||||
ProdWorkerSocketData,
|
||||
ProdWorkerToCoordinatorMessages,
|
||||
ZodNamespace,
|
||||
ZodSocketConnection,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { HttpReply, getTextBody, SimpleLogger } from "@trigger.dev/core-apps";
|
||||
|
||||
import { collectDefaultMetrics, register, Gauge } from "prom-client";
|
||||
collectDefaultMetrics();
|
||||
|
||||
const HTTP_SERVER_PORT = Number(process.env.HTTP_SERVER_PORT || 8020);
|
||||
const NODE_NAME = process.env.NODE_NAME || "coordinator";
|
||||
const REGISTRY_HOST = process.env.REGISTRY_HOST || "localhost:5000";
|
||||
const CHECKPOINT_PATH = process.env.CHECKPOINT_PATH || "/checkpoints";
|
||||
const REGISTRY_TLS_VERIFY = process.env.REGISTRY_TLS_VERIFY === "false" ? "false" : "true";
|
||||
|
||||
const PLATFORM_ENABLED = ["1", "true"].includes(process.env.PLATFORM_ENABLED ?? "true");
|
||||
const PLATFORM_HOST = process.env.PLATFORM_HOST || "127.0.0.1";
|
||||
const PLATFORM_WS_PORT = process.env.PLATFORM_WS_PORT || 3030;
|
||||
const PLATFORM_SECRET = process.env.PLATFORM_SECRET || "coordinator-secret";
|
||||
const SECURE_CONNECTION = ["1", "true"].includes(process.env.SECURE_CONNECTION ?? "false");
|
||||
|
||||
const logger = new SimpleLogger(`[${NODE_NAME}]`);
|
||||
|
||||
type CheckpointerInitializeReturn = {
|
||||
canCheckpoint: boolean;
|
||||
willSimulate: boolean;
|
||||
};
|
||||
|
||||
type CheckpointAndPushOptions = {
|
||||
runId: string;
|
||||
leaveRunning?: boolean;
|
||||
projectRef: string;
|
||||
deploymentVersion: string;
|
||||
};
|
||||
|
||||
type CheckpointData = {
|
||||
location: string;
|
||||
docker: boolean;
|
||||
};
|
||||
|
||||
class Checkpointer {
|
||||
#initialized = false;
|
||||
#canCheckpoint = false;
|
||||
#dockerMode = !process.env.KUBERNETES_PORT;
|
||||
|
||||
#logger = new SimpleLogger("[checkptr]");
|
||||
#abortControllers = new Map<string, AbortController>();
|
||||
|
||||
constructor(private opts = { forceSimulate: false }) {}
|
||||
|
||||
async initialize(): Promise<CheckpointerInitializeReturn> {
|
||||
if (this.#initialized) {
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
|
||||
this.#logger.log(`${this.#dockerMode ? "Docker" : "Kubernetes"} mode`);
|
||||
|
||||
if (this.#dockerMode) {
|
||||
try {
|
||||
await $`criu --version`;
|
||||
} catch (error) {
|
||||
this.#logger.error("No checkpoint support: Missing CRIU binary");
|
||||
this.#logger.error("Will simulate instead");
|
||||
this.#canCheckpoint = false;
|
||||
this.#initialized = true;
|
||||
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
|
||||
try {
|
||||
await $`docker checkpoint`;
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
"No checkpoint support: Docker needs to have experimental features enabled"
|
||||
);
|
||||
this.#logger.error("Will simulate instead");
|
||||
this.#canCheckpoint = false;
|
||||
this.#initialized = true;
|
||||
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await $`buildah login --get-login ${REGISTRY_HOST}`;
|
||||
} catch (error) {
|
||||
this.#logger.error(`No checkpoint support: Not logged in to registry ${REGISTRY_HOST}`);
|
||||
this.#canCheckpoint = false;
|
||||
this.#initialized = true;
|
||||
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
}
|
||||
|
||||
this.#logger.log(
|
||||
`Full checkpoint support${
|
||||
this.#dockerMode && this.opts.forceSimulate ? " with forced simulation enabled." : "!"
|
||||
}`
|
||||
);
|
||||
|
||||
this.#initialized = true;
|
||||
this.#canCheckpoint = true;
|
||||
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
|
||||
#getInitializeReturn(): CheckpointerInitializeReturn {
|
||||
return {
|
||||
canCheckpoint: this.#canCheckpoint,
|
||||
willSimulate: this.#dockerMode && (!this.#canCheckpoint || this.opts.forceSimulate),
|
||||
};
|
||||
}
|
||||
|
||||
#getImageRef(projectRef: string, deploymentVersion: string, shortCode: string) {
|
||||
return `${REGISTRY_HOST}/trigger/${projectRef}:${deploymentVersion}.prod-${shortCode}`;
|
||||
}
|
||||
|
||||
#getExportLocation(projectRef: string, deploymentVersion: string, shortCode: string) {
|
||||
const basename = `${projectRef}-${deploymentVersion}-${shortCode}`;
|
||||
|
||||
if (this.#dockerMode) {
|
||||
return basename;
|
||||
} else {
|
||||
return `${CHECKPOINT_PATH}/${basename}.tar`;
|
||||
}
|
||||
}
|
||||
|
||||
async checkpointAndPush(opts: CheckpointAndPushOptions): Promise<CheckpointData | undefined> {
|
||||
const start = performance.now();
|
||||
logger.log(`checkpointAndPush() start`, { start, opts });
|
||||
|
||||
const result = await this.#checkpointAndPush(opts);
|
||||
|
||||
const end = performance.now();
|
||||
logger.log(`checkpointAndPush() end`, {
|
||||
start,
|
||||
end,
|
||||
diff: end - start,
|
||||
opts,
|
||||
success: !!result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isCheckpointing(runId: string) {
|
||||
return this.#abortControllers.has(runId);
|
||||
}
|
||||
|
||||
cancelCheckpoint(runId: string) {
|
||||
const controller = this.#abortControllers.get(runId);
|
||||
|
||||
if (!controller) {
|
||||
logger.debug("Nothing to cancel", { runId });
|
||||
return;
|
||||
}
|
||||
|
||||
controller.abort("cancelCheckpointing()");
|
||||
this.#abortControllers.delete(runId);
|
||||
}
|
||||
|
||||
async #checkpointAndPush({
|
||||
runId,
|
||||
leaveRunning = true, // This mirrors kubernetes behaviour more accurately
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
}: CheckpointAndPushOptions): Promise<CheckpointData | undefined> {
|
||||
await this.initialize();
|
||||
|
||||
if (!this.#dockerMode && !this.#canCheckpoint) {
|
||||
this.#logger.error("No checkpoint support. Simulation requires docker.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#abortControllers.has(runId)) {
|
||||
logger.error("Checkpoint procedure already in progress", {
|
||||
options: {
|
||||
runId,
|
||||
leaveRunning,
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
this.#abortControllers.set(runId, controller);
|
||||
|
||||
const $$ = $({ signal: controller.signal });
|
||||
|
||||
try {
|
||||
const shortCode = nanoid(8);
|
||||
const imageRef = this.#getImageRef(projectRef, deploymentVersion, shortCode);
|
||||
const exportLocation = this.#getExportLocation(projectRef, deploymentVersion, shortCode);
|
||||
|
||||
this.#logger.log("Checkpointing:", {
|
||||
options: {
|
||||
runId,
|
||||
leaveRunning,
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
},
|
||||
});
|
||||
|
||||
const containterName = this.#getRunContainerName(runId);
|
||||
|
||||
// Create checkpoint (docker)
|
||||
if (this.#dockerMode) {
|
||||
try {
|
||||
if (this.opts.forceSimulate || !this.#canCheckpoint) {
|
||||
this.#logger.log("Simulating checkpoint");
|
||||
this.#logger.debug(await $$`docker pause ${containterName}`);
|
||||
} else {
|
||||
if (leaveRunning) {
|
||||
this.#logger.debug(
|
||||
await $$`docker checkpoint create --leave-running ${containterName} ${exportLocation}`
|
||||
);
|
||||
} else {
|
||||
this.#logger.debug(
|
||||
await $$`docker checkpoint create ${containterName} ${exportLocation}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.#logger.error(error.stderr);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#logger.log("checkpoint created:", {
|
||||
runId,
|
||||
location: exportLocation,
|
||||
});
|
||||
|
||||
return {
|
||||
location: exportLocation,
|
||||
docker: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Create checkpoint (CRI)
|
||||
if (!this.#canCheckpoint) {
|
||||
throw new Error("No checkpoint support in kubernetes mode.");
|
||||
}
|
||||
|
||||
const containerId = this.#logger.debug(
|
||||
// @ts-expect-error
|
||||
await $$`crictl ps`
|
||||
.pipeStdout($$({ stdin: "pipe" })`grep ${containterName}`)
|
||||
.pipeStdout($$({ stdin: "pipe" })`cut -f1 ${"-d "}`)
|
||||
);
|
||||
|
||||
if (!containerId.stdout) {
|
||||
throw new Error("could not find container id");
|
||||
}
|
||||
|
||||
this.#logger.debug(await $$`crictl checkpoint --export=${exportLocation} ${containerId}`);
|
||||
|
||||
// Create image from checkpoint
|
||||
const container = this.#logger.debug(await $$`buildah from scratch`);
|
||||
this.#logger.debug(await $$`buildah add ${container} ${exportLocation} /`);
|
||||
this.#logger.debug(
|
||||
await $$`buildah config --annotation=io.kubernetes.cri-o.annotations.checkpoint.name=counter ${container}`
|
||||
);
|
||||
this.#logger.debug(await $$`buildah commit ${container} ${imageRef}`);
|
||||
this.#logger.debug(await $$`buildah rm ${container}`);
|
||||
|
||||
// Push checkpoint image
|
||||
this.#logger.debug(await $$`buildah push --tls-verify=${REGISTRY_TLS_VERIFY} ${imageRef}`);
|
||||
|
||||
this.#logger.log("Checkpointed and pushed image to:", { location: imageRef });
|
||||
|
||||
try {
|
||||
await $$`rm ${exportLocation}`;
|
||||
this.#logger.log("Deleted checkpoint archive", { exportLocation });
|
||||
|
||||
// Disabled for now as this will increase restore time by having to pull the image again
|
||||
// await $`buildah rmi ${imageRef}`;
|
||||
// this.#logger.log("Deleted checkpoint image", { imageRef });
|
||||
} catch (error) {
|
||||
this.#logger.error("Failed during checkpoint cleanup", { exportLocation });
|
||||
this.#logger.debug(error);
|
||||
}
|
||||
|
||||
return {
|
||||
location: imageRef,
|
||||
docker: false,
|
||||
};
|
||||
} catch (error) {
|
||||
this.#logger.error("checkpoint failed", {
|
||||
options: {
|
||||
runId,
|
||||
leaveRunning,
|
||||
projectRef,
|
||||
deploymentVersion,
|
||||
},
|
||||
error,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
this.#abortControllers.delete(runId);
|
||||
}
|
||||
}
|
||||
|
||||
#getRunContainerName(suffix: string) {
|
||||
return `task-run-${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
class TaskCoordinator {
|
||||
#httpServer: ReturnType<typeof createServer>;
|
||||
#checkpointer = new Checkpointer({ forceSimulate: true });
|
||||
|
||||
#prodWorkerNamespace: ZodNamespace<
|
||||
typeof ProdWorkerToCoordinatorMessages,
|
||||
typeof CoordinatorToProdWorkerMessages,
|
||||
typeof ProdWorkerSocketData
|
||||
>;
|
||||
#platformSocket?: ZodSocketConnection<
|
||||
typeof CoordinatorToPlatformMessages,
|
||||
typeof PlatformToCoordinatorMessages
|
||||
>;
|
||||
|
||||
#checkpointableTasks = new Map<
|
||||
string,
|
||||
{ resolve: (value: void) => void; reject: (err?: any) => void }
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private port: number,
|
||||
private host = "0.0.0.0"
|
||||
) {
|
||||
this.#httpServer = this.#createHttpServer();
|
||||
this.#checkpointer.initialize();
|
||||
|
||||
const io = new Server(this.#httpServer);
|
||||
this.#prodWorkerNamespace = this.#createProdWorkerNamespace(io);
|
||||
|
||||
this.#platformSocket = this.#createPlatformSocket();
|
||||
|
||||
const connectedTasksTotal = new Gauge({
|
||||
name: "daemon_connected_tasks_total", // don't change this without updating dashboard config
|
||||
help: "The number of tasks currently connected.",
|
||||
collect: () => {
|
||||
connectedTasksTotal.set(this.#prodWorkerNamespace.namespace.sockets.size);
|
||||
},
|
||||
});
|
||||
register.registerMetric(connectedTasksTotal);
|
||||
}
|
||||
|
||||
#createPlatformSocket() {
|
||||
if (!PLATFORM_ENABLED) {
|
||||
console.log("INFO: platform connection disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
const platformConnection = new ZodSocketConnection({
|
||||
namespace: "coordinator",
|
||||
host: PLATFORM_HOST,
|
||||
port: Number(PLATFORM_WS_PORT),
|
||||
secure: SECURE_CONNECTION,
|
||||
clientMessages: CoordinatorToPlatformMessages,
|
||||
serverMessages: PlatformToCoordinatorMessages,
|
||||
authToken: PLATFORM_SECRET,
|
||||
handlers: {
|
||||
RESUME_AFTER_DEPENDENCY: async (message) => {
|
||||
const taskSocket = await this.#getAttemptSocket(message.attemptFriendlyId);
|
||||
|
||||
if (!taskSocket) {
|
||||
logger.log("Socket for attempt not found", {
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// In case the task resumed faster than we could checkpoint
|
||||
this.#cancelCheckpoint(message.runId);
|
||||
|
||||
taskSocket.emit("RESUME_AFTER_DEPENDENCY", message);
|
||||
},
|
||||
RESUME_AFTER_DURATION: async (message) => {
|
||||
const taskSocket = await this.#getAttemptSocket(message.attemptFriendlyId);
|
||||
|
||||
if (!taskSocket) {
|
||||
logger.log("Socket for attempt not found", {
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
taskSocket.emit("RESUME_AFTER_DURATION", message);
|
||||
},
|
||||
REQUEST_ATTEMPT_CANCELLATION: async (message) => {
|
||||
const taskSocket = await this.#getAttemptSocket(message.attemptFriendlyId);
|
||||
|
||||
if (!taskSocket) {
|
||||
logger.log("Socket for attempt not found", {
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
taskSocket.emit("REQUEST_ATTEMPT_CANCELLATION", message);
|
||||
},
|
||||
READY_FOR_RETRY: async (message) => {
|
||||
const taskSocket = await this.#getRunSocket(message.runId);
|
||||
|
||||
if (!taskSocket) {
|
||||
logger.log("Socket for attempt not found", {
|
||||
runId: message.runId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
taskSocket.emit("READY_FOR_RETRY", message);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return platformConnection;
|
||||
}
|
||||
|
||||
async #getRunSocket(runId: string) {
|
||||
const sockets = await this.#prodWorkerNamespace.fetchSockets();
|
||||
|
||||
for (const socket of sockets) {
|
||||
if (socket.data.runId === runId) {
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #getAttemptSocket(attemptFriendlyId: string) {
|
||||
const sockets = await this.#prodWorkerNamespace.fetchSockets();
|
||||
|
||||
for (const socket of sockets) {
|
||||
if (socket.data.attemptFriendlyId === attemptFriendlyId) {
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#createProdWorkerNamespace(io: Server) {
|
||||
const provider = new ZodNamespace({
|
||||
io,
|
||||
name: "prod-worker",
|
||||
clientMessages: ProdWorkerToCoordinatorMessages,
|
||||
serverMessages: CoordinatorToProdWorkerMessages,
|
||||
socketData: ProdWorkerSocketData,
|
||||
postAuth: async (socket, next, logger) => {
|
||||
function setSocketDataFromHeader(
|
||||
dataKey: keyof typeof socket.data,
|
||||
headerName: string,
|
||||
required: boolean = true
|
||||
) {
|
||||
const value = socket.handshake.headers[headerName];
|
||||
|
||||
if (value) {
|
||||
socket.data[dataKey] = Array.isArray(value) ? value[0] : value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (required) {
|
||||
logger.error("missing required header", { headerName });
|
||||
throw new Error("missing header");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setSocketDataFromHeader("podName", "x-pod-name");
|
||||
setSocketDataFromHeader("contentHash", "x-trigger-content-hash");
|
||||
setSocketDataFromHeader("projectRef", "x-trigger-project-ref");
|
||||
setSocketDataFromHeader("runId", "x-trigger-run-id");
|
||||
setSocketDataFromHeader("attemptFriendlyId", "x-trigger-attempt-friendly-id", false);
|
||||
setSocketDataFromHeader("envId", "x-trigger-env-id");
|
||||
setSocketDataFromHeader("deploymentId", "x-trigger-deployment-id");
|
||||
setSocketDataFromHeader("deploymentVersion", "x-trigger-deployment-version");
|
||||
} catch (error) {
|
||||
logger.error("setSocketDataFromHeader error", { error });
|
||||
socket.disconnect(true);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("success", socket.data);
|
||||
|
||||
next();
|
||||
},
|
||||
onConnection: async (socket, handler, sender) => {
|
||||
const logger = new SimpleLogger(`[prod-worker][${socket.id}]`);
|
||||
|
||||
const checkpointInProgress = () => {
|
||||
return this.#checkpointableTasks.has(socket.data.runId);
|
||||
};
|
||||
|
||||
const readyToCheckpoint = async (): Promise<
|
||||
{ success: true } | { success: false; reason?: string }
|
||||
> => {
|
||||
if (checkpointInProgress()) {
|
||||
return {
|
||||
success: false,
|
||||
reason: "checkpoint in progress",
|
||||
};
|
||||
}
|
||||
|
||||
const isCheckpointable = new Promise((resolve, reject) => {
|
||||
// We set a reasonable timeout to prevent waiting forever
|
||||
// TODO: We may also want to cancel the task as it's unlikely to recover
|
||||
setTimeout(() => reject("timeout"), 10_000);
|
||||
|
||||
this.#checkpointableTasks.set(socket.data.runId, { resolve, reject });
|
||||
});
|
||||
|
||||
try {
|
||||
await isCheckpointable;
|
||||
this.#checkpointableTasks.delete(socket.data.runId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error while waiting for checkpointable state", { error });
|
||||
|
||||
return {
|
||||
success: false,
|
||||
reason: typeof error === "string" ? error : "unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this.#platformSocket?.send("LOG", {
|
||||
metadata: socket.data,
|
||||
text: "connected",
|
||||
});
|
||||
|
||||
socket.on("LOG", (message, callback) => {
|
||||
logger.log("[LOG]", message.text);
|
||||
|
||||
callback();
|
||||
|
||||
this.#platformSocket?.send("LOG", {
|
||||
version: "v1",
|
||||
metadata: socket.data,
|
||||
text: message.text,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("READY_FOR_EXECUTION", async (message) => {
|
||||
logger.log("[READY_FOR_EXECUTION]", message);
|
||||
|
||||
try {
|
||||
const executionAck = await this.#platformSocket?.sendWithAck(
|
||||
"READY_FOR_EXECUTION",
|
||||
message
|
||||
);
|
||||
|
||||
if (!executionAck) {
|
||||
logger.error("no execution ack", { runId: socket.data.runId });
|
||||
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!executionAck.success) {
|
||||
logger.error("failed to get execution payload", { runId: socket.data.runId });
|
||||
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit("EXECUTE_TASK_RUN", {
|
||||
version: "v1",
|
||||
executionPayload: executionAck.payload,
|
||||
});
|
||||
|
||||
socket.data.attemptFriendlyId = executionAck.payload.execution.attempt.id;
|
||||
} catch (error) {
|
||||
logger.error("Error", { error });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("READY_FOR_RESUME", async (message) => {
|
||||
logger.log("[READY_FOR_RESUME]", message);
|
||||
|
||||
socket.data.attemptFriendlyId = message.attemptFriendlyId;
|
||||
this.#platformSocket?.send("READY_FOR_RESUME", message);
|
||||
});
|
||||
|
||||
socket.on("TASK_RUN_COMPLETED", async ({ completion, execution }, callback) => {
|
||||
logger.log("completed task", { completionId: completion.id });
|
||||
|
||||
const completeWithoutCheckpoint = (shouldExit: boolean) => {
|
||||
this.#platformSocket?.send("TASK_RUN_COMPLETED", {
|
||||
version: "v1",
|
||||
execution,
|
||||
completion,
|
||||
});
|
||||
callback({ willCheckpointAndRestore: false, shouldExit });
|
||||
};
|
||||
|
||||
if (completion.ok) {
|
||||
completeWithoutCheckpoint(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
completion.error.type === "INTERNAL_ERROR" &&
|
||||
completion.error.code === "TASK_RUN_CANCELLED"
|
||||
) {
|
||||
completeWithoutCheckpoint(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (completion.retry === undefined) {
|
||||
completeWithoutCheckpoint(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (completion.retry.delay < 10_000) {
|
||||
completeWithoutCheckpoint(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { canCheckpoint, willSimulate } = await this.#checkpointer.initialize();
|
||||
|
||||
const willCheckpointAndRestore = canCheckpoint || willSimulate;
|
||||
|
||||
if (!willCheckpointAndRestore) {
|
||||
completeWithoutCheckpoint(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// The worker will then put itself in a checkpointable state
|
||||
callback({ willCheckpointAndRestore: true, shouldExit: false });
|
||||
|
||||
const ready = await readyToCheckpoint();
|
||||
|
||||
if (!ready.success) {
|
||||
logger.error("Failed to become checkpointable", {
|
||||
runId: socket.data.runId,
|
||||
reason: ready.reason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const checkpoint = await this.#checkpointer.checkpointAndPush({
|
||||
runId: socket.data.runId,
|
||||
projectRef: socket.data.projectRef,
|
||||
deploymentVersion: socket.data.deploymentVersion,
|
||||
});
|
||||
|
||||
if (!checkpoint) {
|
||||
logger.error("Failed to checkpoint", { runId: socket.data.runId });
|
||||
completeWithoutCheckpoint(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("TASK_RUN_COMPLETED", {
|
||||
version: "v1",
|
||||
execution,
|
||||
completion,
|
||||
checkpoint,
|
||||
});
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("READY_FOR_CHECKPOINT", async (message) => {
|
||||
logger.log("[READY_FOR_CHECKPOINT]", message);
|
||||
|
||||
const checkpointable = this.#checkpointableTasks.get(socket.data.runId);
|
||||
|
||||
if (!checkpointable) {
|
||||
logger.error("No checkpoint scheduled", { runId: socket.data.runId });
|
||||
return;
|
||||
}
|
||||
|
||||
checkpointable.resolve();
|
||||
});
|
||||
|
||||
socket.on("CANCEL_CHECKPOINT", async (message) => {
|
||||
logger.log("[CANCEL_CHECKPOINT]", message);
|
||||
|
||||
this.#cancelCheckpoint(socket.data.runId);
|
||||
});
|
||||
|
||||
socket.on("WAIT_FOR_DURATION", async (message, callback) => {
|
||||
logger.log("[WAIT_FOR_DURATION]", message);
|
||||
|
||||
if (checkpointInProgress()) {
|
||||
logger.error("Checkpoint already in progress", { runId: socket.data.runId });
|
||||
callback({ willCheckpointAndRestore: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const { canCheckpoint, willSimulate } = await this.#checkpointer.initialize();
|
||||
|
||||
const willCheckpointAndRestore = canCheckpoint || willSimulate;
|
||||
|
||||
callback({ willCheckpointAndRestore });
|
||||
|
||||
if (!willCheckpointAndRestore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ready = await readyToCheckpoint();
|
||||
|
||||
if (!ready.success) {
|
||||
logger.error("Failed to become checkpointable", {
|
||||
runId: socket.data.runId,
|
||||
reason: ready.reason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const checkpoint = await this.#checkpointer.checkpointAndPush({
|
||||
runId: socket.data.runId,
|
||||
projectRef: socket.data.projectRef,
|
||||
deploymentVersion: socket.data.deploymentVersion,
|
||||
});
|
||||
|
||||
if (!checkpoint) {
|
||||
// The task container will keep running until the wait duration has elapsed
|
||||
logger.error("Failed to checkpoint", { runId: socket.data.runId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("CHECKPOINT_CREATED", {
|
||||
version: "v1",
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
docker: checkpoint.docker,
|
||||
location: checkpoint.location,
|
||||
reason: {
|
||||
type: "WAIT_FOR_DURATION",
|
||||
ms: message.ms,
|
||||
now: message.now,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("WAIT_FOR_TASK", async (message, callback) => {
|
||||
logger.log("[WAIT_FOR_TASK]", message);
|
||||
|
||||
const { canCheckpoint, willSimulate } = await this.#checkpointer.initialize();
|
||||
|
||||
const willCheckpointAndRestore = canCheckpoint || willSimulate;
|
||||
|
||||
callback({ willCheckpointAndRestore });
|
||||
|
||||
if (!willCheckpointAndRestore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkpoint = await this.#checkpointer.checkpointAndPush({
|
||||
runId: socket.data.runId,
|
||||
projectRef: socket.data.projectRef,
|
||||
deploymentVersion: socket.data.deploymentVersion,
|
||||
});
|
||||
|
||||
if (!checkpoint) {
|
||||
logger.error("Failed to checkpoint", { runId: socket.data.runId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("CHECKPOINT_CREATED", {
|
||||
version: "v1",
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
docker: checkpoint.docker,
|
||||
location: checkpoint.location,
|
||||
reason: {
|
||||
type: "WAIT_FOR_TASK",
|
||||
friendlyId: message.friendlyId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("WAIT_FOR_BATCH", async (message, callback) => {
|
||||
logger.log("[WAIT_FOR_BATCH]", message);
|
||||
|
||||
const { canCheckpoint, willSimulate } = await this.#checkpointer.initialize();
|
||||
|
||||
const willCheckpointAndRestore = canCheckpoint || willSimulate;
|
||||
|
||||
callback({ willCheckpointAndRestore });
|
||||
|
||||
if (!willCheckpointAndRestore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkpoint = await this.#checkpointer.checkpointAndPush({
|
||||
runId: socket.data.runId,
|
||||
projectRef: socket.data.projectRef,
|
||||
deploymentVersion: socket.data.deploymentVersion,
|
||||
});
|
||||
|
||||
if (!checkpoint) {
|
||||
logger.error("Failed to checkpoint", { runId: socket.data.runId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("CHECKPOINT_CREATED", {
|
||||
version: "v1",
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
docker: checkpoint.docker,
|
||||
location: checkpoint.location,
|
||||
reason: {
|
||||
type: "WAIT_FOR_BATCH",
|
||||
batchFriendlyId: message.batchFriendlyId,
|
||||
runFriendlyIds: message.runFriendlyIds,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("INDEX_TASKS", async (message, callback) => {
|
||||
logger.log("[INDEX_TASKS]", message);
|
||||
|
||||
const workerAck = await this.#platformSocket?.sendWithAck("CREATE_WORKER", {
|
||||
version: "v1",
|
||||
projectRef: socket.data.projectRef,
|
||||
envId: socket.data.envId,
|
||||
deploymentId: message.deploymentId,
|
||||
metadata: {
|
||||
contentHash: socket.data.contentHash,
|
||||
packageVersion: message.packageVersion,
|
||||
tasks: message.tasks,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workerAck) {
|
||||
logger.debug("no worker ack while indexing", message);
|
||||
}
|
||||
|
||||
callback({ success: !!workerAck?.success });
|
||||
});
|
||||
|
||||
socket.on("INDEXING_FAILED", async (message) => {
|
||||
logger.log("[INDEXING_FAILED]", message);
|
||||
|
||||
this.#platformSocket?.send("INDEXING_FAILED", {
|
||||
version: "v1",
|
||||
deploymentId: message.deploymentId,
|
||||
error: message.error,
|
||||
});
|
||||
});
|
||||
},
|
||||
onDisconnect: async (socket, handler, sender, logger) => {
|
||||
this.#platformSocket?.send("LOG", {
|
||||
metadata: socket.data,
|
||||
text: "disconnect",
|
||||
});
|
||||
},
|
||||
handlers: {
|
||||
TASK_HEARTBEAT: async (message) => {
|
||||
this.#platformSocket?.send("TASK_HEARTBEAT", message);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
#cancelCheckpoint(runId: string) {
|
||||
const checkpointWait = this.#checkpointableTasks.get(runId);
|
||||
|
||||
if (checkpointWait) {
|
||||
// Stop waiting for task to reach checkpointable state
|
||||
checkpointWait.reject("Checkpoint cancelled");
|
||||
}
|
||||
|
||||
// Cancel checkpointing procedure
|
||||
this.#checkpointer.cancelCheckpoint(runId);
|
||||
}
|
||||
|
||||
#createHttpServer() {
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
logger.log(`[${req.method}]`, req.url);
|
||||
|
||||
const reply = new HttpReply(res);
|
||||
|
||||
switch (req.url) {
|
||||
case "/health": {
|
||||
return reply.text("ok");
|
||||
}
|
||||
case "/metrics": {
|
||||
return reply.text(await register.metrics(), 200, register.contentType);
|
||||
}
|
||||
case "/whoami": {
|
||||
return reply.text(NODE_NAME);
|
||||
}
|
||||
case "/checkpoint": {
|
||||
const body = await getTextBody(req);
|
||||
// await this.#checkpointer.checkpointAndPush(body);
|
||||
return reply.text(`sent restore request: ${body}`);
|
||||
}
|
||||
default: {
|
||||
return reply.empty(404);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
httpServer.on("clientError", (err, socket) => {
|
||||
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
||||
});
|
||||
|
||||
httpServer.on("listening", () => {
|
||||
logger.log("server listening on port", HTTP_SERVER_PORT);
|
||||
});
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.#httpServer.listen(this.port, this.host);
|
||||
}
|
||||
}
|
||||
|
||||
const coordinator = new TaskCoordinator(HTTP_SERVER_PORT);
|
||||
coordinator.listen();
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@trigger.dev/core/v3": ["../../packages/core/src/v3"],
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"],
|
||||
"@trigger.dev/core-apps": ["../../packages/core-apps/src"],
|
||||
"@trigger.dev/core-apps/*": ["../../packages/core-apps/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
HTTP_SERVER_PORT=8050
|
||||
|
||||
PLATFORM_WS_PORT=3030
|
||||
PLATFORM_SECRET=provider-secret
|
||||
SECURE_CONNECTION=false
|
||||
|
||||
# Use this if you are on macOS
|
||||
# COORDINATOR_HOST="host.docker.internal"
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT="http://host.docker.internal:4318"
|
||||
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.env
|
||||
@@ -0,0 +1,16 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
FROM node:18-slim AS base
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y dumb-init
|
||||
|
||||
FROM base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node dist/index.mjs /app/
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/dumb-init", "--", "/usr/local/bin/node", "/app/index.mjs" ]
|
||||
@@ -0,0 +1,3 @@
|
||||
# Docker provider
|
||||
|
||||
The `docker-provider` allows the platform to be orchestrator-agnostic. The platform can perform actions such as `INDEX_TASKS` or `INVOKE_TASK` which the provider translates into Docker actions.
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "docker-provider",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.cjs",
|
||||
"scripts": {
|
||||
"build": "npm run build:bundle",
|
||||
"build:bundle": "esbuild src/index.ts --bundle --outfile=dist/index.mjs --platform=node --format=esm --target=esnext --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);\"",
|
||||
"build:image": "docker build -f Containerfile . -t docker-provider",
|
||||
"dev": "tsx --no-warnings=ExperimentalWarning --require dotenv/config --watch src/index.ts",
|
||||
"start": "tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"execa": "^8.0.1",
|
||||
"socket.io-client": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.19.8",
|
||||
"dotenv": "^16.4.2",
|
||||
"esbuild": "^0.19.11",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
import { $, type ExecaChildProcess, execa } from "execa";
|
||||
import {
|
||||
SimpleLogger,
|
||||
TaskOperations,
|
||||
ProviderShell,
|
||||
TaskOperationsRestoreOptions,
|
||||
TaskOperationsCreateOptions,
|
||||
TaskOperationsIndexOptions,
|
||||
} from "@trigger.dev/core-apps";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import { PostStartCauses, PreStopCauses } from "@trigger.dev/core/v3";
|
||||
|
||||
const MACHINE_NAME = process.env.MACHINE_NAME || "local";
|
||||
const COORDINATOR_PORT = process.env.COORDINATOR_PORT || 8020;
|
||||
const COORDINATOR_HOST = process.env.COORDINATOR_HOST || "127.0.0.1";
|
||||
const OTEL_EXPORTER_OTLP_ENDPOINT =
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://0.0.0.0:4318";
|
||||
|
||||
const logger = new SimpleLogger(`[${MACHINE_NAME}]`);
|
||||
|
||||
type InitializeReturn = {
|
||||
canCheckpoint: boolean;
|
||||
willSimulate: boolean;
|
||||
};
|
||||
|
||||
function isExecaChildProcess(maybeExeca: unknown): maybeExeca is Awaited<ExecaChildProcess> {
|
||||
return typeof maybeExeca === "object" && maybeExeca !== null && "escapedCommand" in maybeExeca;
|
||||
}
|
||||
|
||||
class DockerTaskOperations implements TaskOperations {
|
||||
#initialized = false;
|
||||
#canCheckpoint = false;
|
||||
|
||||
constructor(private opts = { forceSimulate: false }) {}
|
||||
|
||||
async #initialize(): Promise<InitializeReturn> {
|
||||
if (this.#initialized) {
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
|
||||
logger.log("Initializing task operations");
|
||||
|
||||
if (this.opts.forceSimulate) {
|
||||
logger.log("Forced simulation enabled. Will simulate regardless of checkpoint support.");
|
||||
}
|
||||
|
||||
try {
|
||||
await $`criu --version`;
|
||||
} catch (error) {
|
||||
logger.error("No checkpoint support: Missing CRIU binary. Will simulate instead.");
|
||||
this.#canCheckpoint = false;
|
||||
this.#initialized = true;
|
||||
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
|
||||
try {
|
||||
await $`docker checkpoint`;
|
||||
} catch (error) {
|
||||
logger.error("No checkpoint support: Docker needs to have experimental features enabled");
|
||||
logger.error("Will simulate instead");
|
||||
this.#canCheckpoint = false;
|
||||
this.#initialized = true;
|
||||
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
|
||||
logger.log("Full checkpoint support!");
|
||||
|
||||
this.#initialized = true;
|
||||
this.#canCheckpoint = true;
|
||||
|
||||
return this.#getInitializeReturn();
|
||||
}
|
||||
|
||||
#getInitializeReturn(): InitializeReturn {
|
||||
return {
|
||||
canCheckpoint: this.#canCheckpoint,
|
||||
willSimulate: !this.#canCheckpoint || this.opts.forceSimulate,
|
||||
};
|
||||
}
|
||||
|
||||
async index(opts: TaskOperationsIndexOptions) {
|
||||
await this.#initialize();
|
||||
|
||||
const containerName = this.#getIndexContainerName(opts.shortCode);
|
||||
|
||||
logger.log(`Indexing task ${opts.imageRef}`, {
|
||||
host: COORDINATOR_HOST,
|
||||
port: COORDINATOR_PORT,
|
||||
});
|
||||
|
||||
try {
|
||||
logger.debug(
|
||||
await execa("docker", [
|
||||
"run",
|
||||
"--network=host",
|
||||
"--rm",
|
||||
`--env=INDEX_TASKS=true`,
|
||||
`--env=TRIGGER_SECRET_KEY=${opts.apiKey}`,
|
||||
`--env=TRIGGER_API_URL=${opts.apiUrl}`,
|
||||
`--env=TRIGGER_ENV_ID=${opts.envId}`,
|
||||
`--env=OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT}`,
|
||||
`--env=POD_NAME=${containerName}`,
|
||||
`--env=COORDINATOR_HOST=${COORDINATOR_HOST}`,
|
||||
`--env=COORDINATOR_PORT=${COORDINATOR_PORT}`,
|
||||
`--name=${containerName}`,
|
||||
`${opts.imageRef}`,
|
||||
])
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (!isExecaChildProcess(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.error("Index failed:", {
|
||||
opts,
|
||||
exitCode: error.exitCode,
|
||||
escapedCommand: error.escapedCommand,
|
||||
stdout: error.stdout,
|
||||
stderr: error.stderr,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async create(opts: TaskOperationsCreateOptions) {
|
||||
await this.#initialize();
|
||||
|
||||
const containerName = this.#getRunContainerName(opts.runId);
|
||||
|
||||
try {
|
||||
logger.debug(
|
||||
await execa("docker", [
|
||||
"run",
|
||||
"--network=host",
|
||||
"--detach",
|
||||
`--env=TRIGGER_ENV_ID=${opts.envId}`,
|
||||
`--env=TRIGGER_RUN_ID=${opts.runId}`,
|
||||
`--env=OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT}`,
|
||||
`--env=POD_NAME=${containerName}`,
|
||||
`--env=COORDINATOR_HOST=${COORDINATOR_HOST}`,
|
||||
`--env=COORDINATOR_PORT=${COORDINATOR_PORT}`,
|
||||
`--name=${containerName}`,
|
||||
`${opts.image}`,
|
||||
])
|
||||
);
|
||||
} catch (error) {
|
||||
if (!isExecaChildProcess(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.error("Create failed:", {
|
||||
opts,
|
||||
exitCode: error.exitCode,
|
||||
escapedCommand: error.escapedCommand,
|
||||
stdout: error.stdout,
|
||||
stderr: error.stderr,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async restore(opts: TaskOperationsRestoreOptions) {
|
||||
await this.#initialize();
|
||||
|
||||
const containerName = this.#getRunContainerName(opts.runId);
|
||||
|
||||
if (!this.#canCheckpoint || this.opts.forceSimulate) {
|
||||
logger.log("Simulating restore");
|
||||
|
||||
const unpause = logger.debug(await $`docker unpause ${containerName}`);
|
||||
|
||||
if (unpause.exitCode !== 0) {
|
||||
throw new Error("docker unpause command failed");
|
||||
}
|
||||
|
||||
await this.#sendPostStart(containerName);
|
||||
return;
|
||||
}
|
||||
|
||||
const { exitCode } = logger.debug(
|
||||
await $`docker start --checkpoint=${opts.checkpointRef} ${containerName}`
|
||||
);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error("docker start command failed");
|
||||
}
|
||||
|
||||
await this.#sendPostStart(containerName);
|
||||
}
|
||||
|
||||
async delete(opts: { runId: string }) {
|
||||
await this.#initialize();
|
||||
|
||||
const containerName = this.#getRunContainerName(opts.runId);
|
||||
await this.#sendPreStop(containerName);
|
||||
|
||||
logger.log("noop: delete");
|
||||
}
|
||||
|
||||
async get(opts: { runId: string }) {
|
||||
await this.#initialize();
|
||||
|
||||
logger.log("noop: get");
|
||||
}
|
||||
|
||||
#getIndexContainerName(suffix: string) {
|
||||
return `task-index-${suffix}`;
|
||||
}
|
||||
|
||||
#getRunContainerName(suffix: string) {
|
||||
return `task-run-${suffix}`;
|
||||
}
|
||||
|
||||
async #sendPostStart(containerName: string): Promise<void> {
|
||||
try {
|
||||
const port = await this.#getHttpServerPort(containerName);
|
||||
logger.debug(await this.#runLifecycleCommand(containerName, port, "postStart", "restore"));
|
||||
} catch (error) {
|
||||
logger.error("postStart error", { error });
|
||||
throw new Error("postStart command failed");
|
||||
}
|
||||
}
|
||||
|
||||
async #sendPreStop(containerName: string): Promise<void> {
|
||||
try {
|
||||
const port = await this.#getHttpServerPort(containerName);
|
||||
logger.debug(await this.#runLifecycleCommand(containerName, port, "preStop", "terminate"));
|
||||
} catch (error) {
|
||||
logger.error("preStop error", { error });
|
||||
throw new Error("preStop command failed");
|
||||
}
|
||||
}
|
||||
|
||||
async #getHttpServerPort(containerName: string): Promise<number> {
|
||||
// We first get the correct port, which is random during dev as we run with host networking and need to avoid clashes
|
||||
// FIXME: Skip this in prod
|
||||
const logs = logger.debug(await $`docker logs ${containerName}`);
|
||||
const matches = logs.stdout.match(/http server listening on port (?<port>[0-9]+)/);
|
||||
|
||||
const port = Number(matches?.groups?.port);
|
||||
|
||||
if (!port) {
|
||||
throw new Error("failed to extract port from logs");
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
async #runLifecycleCommand<THookType extends "postStart" | "preStop">(
|
||||
containerName: string,
|
||||
port: number,
|
||||
type: THookType,
|
||||
cause: THookType extends "postStart" ? PostStartCauses : PreStopCauses,
|
||||
retryCount = 0
|
||||
): Promise<ExecaChildProcess> {
|
||||
try {
|
||||
return await execa("docker", [
|
||||
"exec",
|
||||
containerName,
|
||||
"wget",
|
||||
"-q",
|
||||
"-O-",
|
||||
`127.0.0.1:${port}/${type}?cause=${cause}`,
|
||||
]);
|
||||
} catch (error: any) {
|
||||
if (type === "postStart" && retryCount < 6) {
|
||||
logger.debug(`retriable ${type} error`, { retryCount, message: error?.message });
|
||||
await setTimeout(exponentialBackoff(retryCount + 1, 2, 50, 1150, 50));
|
||||
|
||||
return this.#runLifecycleCommand(containerName, port, type, cause, retryCount + 1);
|
||||
}
|
||||
|
||||
logger.error(`final ${type} error`, { message: error?.message });
|
||||
throw new Error(`${type} command failed after ${retryCount - 1} retries`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new ProviderShell({
|
||||
tasks: new DockerTaskOperations({ forceSimulate: true }),
|
||||
type: "docker",
|
||||
});
|
||||
|
||||
provider.listen();
|
||||
|
||||
function exponentialBackoff(
|
||||
retryCount: number,
|
||||
exponential: number,
|
||||
minDelay: number,
|
||||
maxDelay: number,
|
||||
jitter: number
|
||||
): number {
|
||||
// Calculate the delay using the exponential backoff formula
|
||||
const delay = Math.min(Math.pow(exponential, retryCount) * minDelay, maxDelay);
|
||||
|
||||
// Calculate the jitter
|
||||
const jitterValue = Math.random() * jitter;
|
||||
|
||||
// Return the calculated delay with jitter
|
||||
return delay + jitterValue;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@trigger.dev/core/v3": ["../../packages/core/src/v3"],
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"],
|
||||
"@trigger.dev/core-apps": ["../../packages/core-apps/src"],
|
||||
"@trigger.dev/core-apps/*": ["../../packages/core-apps/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
HTTP_SERVER_PORT=8060
|
||||
|
||||
PLATFORM_WS_PORT=3030
|
||||
PLATFORM_SECRET=provider-secret
|
||||
SECURE_CONNECTION=false
|
||||
|
||||
# Use this if you are on macOS
|
||||
# COORDINATOR_HOST="host.docker.internal"
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT="http://host.docker.internal:4318"
|
||||
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.env
|
||||
@@ -0,0 +1,47 @@
|
||||
FROM node:18-alpine@sha256:ca9f6cb0466f9638e59e0c249d335a07c867cd50c429b5c7830dda1bed584649 AS node-18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM node-18-alpine AS pruner
|
||||
|
||||
COPY --chown=node:node . .
|
||||
RUN npx -q turbo@1.10.9 prune --scope=kubernetes-provider --docker
|
||||
RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
|
||||
|
||||
FROM node-18-alpine AS base
|
||||
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
COPY --chown=node:node .gitignore .gitignore
|
||||
COPY --from=pruner --chown=node:node /app/out/json/ .
|
||||
COPY --from=pruner --chown=node:node /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
COPY --from=pruner --chown=node:node /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||
|
||||
FROM base AS dev-deps
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV development
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch --no-frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --ignore-scripts --no-frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
RUN corepack enable
|
||||
|
||||
COPY --from=pruner --chown=node:node /app/out/full/ .
|
||||
COPY --from=dev-deps --chown=node:node /app/ .
|
||||
COPY --chown=node:node turbo.json turbo.json
|
||||
|
||||
RUN pnpm run -r --filter kubernetes-provider build:bundle
|
||||
|
||||
FROM base AS runner
|
||||
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV production
|
||||
|
||||
COPY --from=builder --chown=node:node /app/apps/kubernetes-provider/dist/index.mjs ./index.mjs
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
USER node
|
||||
|
||||
CMD [ "/usr/bin/dumb-init", "--", "/usr/local/bin/node", "./index.mjs" ]
|
||||
@@ -0,0 +1,3 @@
|
||||
# Kubernetes provider
|
||||
|
||||
The `kubernetes-provider` allows the platform to be orchestrator-agnostic. The platform can perform actions such as `INDEX_TASKS` or `INVOKE_TASK` which the provider translates into Kubernetes actions.
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "kubernetes-provider",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.cjs",
|
||||
"scripts": {
|
||||
"build": "npm run build:bundle",
|
||||
"build:bundle": "esbuild src/index.ts --bundle --outfile=dist/index.mjs --platform=node --format=esm --target=esnext --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);\"",
|
||||
"build:image": "docker build -f Containerfile . -t kubernetes-provider",
|
||||
"dev": "tsx --no-warnings=ExperimentalWarning --require dotenv/config --watch src/index.ts",
|
||||
"start": "tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^0.20.0",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"@trigger.dev/core-apps": "workspace:*",
|
||||
"socket.io-client": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.4.2",
|
||||
"esbuild": "^0.19.11",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import {
|
||||
ProviderShell,
|
||||
SimpleLogger,
|
||||
TaskOperations,
|
||||
TaskOperationsCreateOptions,
|
||||
TaskOperationsIndexOptions,
|
||||
TaskOperationsRestoreOptions,
|
||||
} from "@trigger.dev/core-apps";
|
||||
import { Machine, PostStartCauses, PreStopCauses, EnvironmentType } from "@trigger.dev/core/v3";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local";
|
||||
const NODE_NAME = process.env.NODE_NAME || "local";
|
||||
const OTEL_EXPORTER_OTLP_ENDPOINT =
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318";
|
||||
|
||||
const logger = new SimpleLogger(`[${NODE_NAME}]`);
|
||||
logger.log(`running in ${RUNTIME_ENV} mode`);
|
||||
|
||||
type Namespace = {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
class KubernetesTaskOperations implements TaskOperations {
|
||||
#namespace: Namespace;
|
||||
#k8sApi: {
|
||||
core: k8s.CoreV1Api;
|
||||
batch: k8s.BatchV1Api;
|
||||
};
|
||||
|
||||
constructor(namespace = "default") {
|
||||
this.#namespace = {
|
||||
metadata: {
|
||||
name: namespace,
|
||||
},
|
||||
};
|
||||
|
||||
this.#k8sApi = this.#createK8sApi();
|
||||
}
|
||||
|
||||
async index(opts: TaskOperationsIndexOptions) {
|
||||
await this.#createJob(
|
||||
{
|
||||
metadata: {
|
||||
name: this.#getIndexContainerName(opts.shortCode),
|
||||
namespace: this.#namespace.metadata.name,
|
||||
},
|
||||
spec: {
|
||||
completions: 1,
|
||||
backoffLimit: 0,
|
||||
ttlSecondsAfterFinished: 300,
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: "task-index",
|
||||
"app.kubernetes.io/part-of": "trigger-worker",
|
||||
"app.kubernetes.io/component": "index",
|
||||
env: opts.envId,
|
||||
envtype: this.#envTypeToLabelValue(opts.envType),
|
||||
org: opts.orgId,
|
||||
project: opts.projectId,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
restartPolicy: "Never",
|
||||
imagePullSecrets: [
|
||||
{
|
||||
name: "registry-trigger",
|
||||
},
|
||||
],
|
||||
nodeSelector: {
|
||||
nodetype: "worker",
|
||||
},
|
||||
containers: [
|
||||
{
|
||||
name: this.#getIndexContainerName(opts.shortCode),
|
||||
image: opts.imageRef,
|
||||
ports: [
|
||||
{
|
||||
containerPort: 8000,
|
||||
},
|
||||
],
|
||||
// resources: {
|
||||
// limits: {
|
||||
// cpu: "100m",
|
||||
// memory: "50Mi",
|
||||
// },
|
||||
// },
|
||||
lifecycle: {
|
||||
preStop: {
|
||||
exec: {
|
||||
command: this.#getLifecycleCommand("preStop", "terminate"),
|
||||
},
|
||||
},
|
||||
},
|
||||
env: [
|
||||
{
|
||||
name: "DEBUG",
|
||||
value: "true",
|
||||
},
|
||||
{
|
||||
name: "INDEX_TASKS",
|
||||
value: "true",
|
||||
},
|
||||
{
|
||||
name: "TRIGGER_SECRET_KEY",
|
||||
value: opts.apiKey,
|
||||
},
|
||||
{
|
||||
name: "TRIGGER_API_URL",
|
||||
value: opts.apiUrl,
|
||||
},
|
||||
{
|
||||
name: "TRIGGER_ENV_ID",
|
||||
value: opts.envId,
|
||||
},
|
||||
{
|
||||
name: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
||||
value: OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
},
|
||||
{
|
||||
name: "HTTP_SERVER_PORT",
|
||||
value: "8000",
|
||||
},
|
||||
{
|
||||
name: "POD_NAME",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "metadata.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "COORDINATOR_HOST",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "status.hostIP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MACHINE_NAME",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "spec.nodeName",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
this.#namespace
|
||||
);
|
||||
}
|
||||
|
||||
async create(opts: TaskOperationsCreateOptions) {
|
||||
await this.#createPod(
|
||||
{
|
||||
metadata: {
|
||||
name: this.#getRunContainerName(opts.runId),
|
||||
namespace: this.#namespace.metadata.name,
|
||||
labels: {
|
||||
app: "task-run",
|
||||
"app.kubernetes.io/part-of": "trigger-worker",
|
||||
"app.kubernetes.io/component": "create",
|
||||
env: opts.envId,
|
||||
envtype: this.#envTypeToLabelValue(opts.envType),
|
||||
org: opts.orgId,
|
||||
project: opts.projectId,
|
||||
run: opts.runId,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
restartPolicy: "Never",
|
||||
imagePullSecrets: [
|
||||
{
|
||||
name: "registry-trigger",
|
||||
},
|
||||
],
|
||||
nodeSelector: {
|
||||
nodetype: "worker",
|
||||
},
|
||||
containers: [
|
||||
{
|
||||
name: this.#getRunContainerName(opts.runId),
|
||||
image: opts.image,
|
||||
ports: [
|
||||
{
|
||||
containerPort: 8000,
|
||||
},
|
||||
],
|
||||
resources: {
|
||||
limits: this.#getResourcesFromMachineConfig(opts.machine),
|
||||
},
|
||||
lifecycle: {
|
||||
postStart: {
|
||||
exec: {
|
||||
command: this.#getLifecycleCommand("postStart", "create"),
|
||||
},
|
||||
},
|
||||
preStop: {
|
||||
exec: {
|
||||
command: this.#getLifecycleCommand("preStop", "terminate"),
|
||||
},
|
||||
},
|
||||
},
|
||||
env: [
|
||||
{
|
||||
name: "DEBUG",
|
||||
value: "true",
|
||||
},
|
||||
{
|
||||
name: "HTTP_SERVER_PORT",
|
||||
value: "8000",
|
||||
},
|
||||
{
|
||||
name: "TRIGGER_ENV_ID",
|
||||
value: opts.envId,
|
||||
},
|
||||
{
|
||||
name: "TRIGGER_RUN_ID",
|
||||
value: opts.runId,
|
||||
},
|
||||
{
|
||||
name: "TRIGGER_WORKER_VERSION",
|
||||
value: opts.version,
|
||||
},
|
||||
{
|
||||
name: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
||||
value: OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
},
|
||||
{
|
||||
name: "POD_NAME",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "metadata.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "COORDINATOR_HOST",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "status.hostIP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NODE_NAME",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "spec.nodeName",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "taskinfo",
|
||||
mountPath: "/etc/taskinfo",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
volumes: [
|
||||
{
|
||||
name: "taskinfo",
|
||||
emptyDir: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
this.#namespace
|
||||
);
|
||||
}
|
||||
|
||||
async restore(opts: TaskOperationsRestoreOptions) {
|
||||
await this.#createPod(
|
||||
{
|
||||
metadata: {
|
||||
name: `${this.#getRunContainerName(opts.runId)}-${randomUUID().slice(0, 8)}`,
|
||||
namespace: this.#namespace.metadata.name,
|
||||
labels: {
|
||||
app: "task-run",
|
||||
"app.kubernetes.io/part-of": "trigger-worker",
|
||||
"app.kubernetes.io/component": "restore",
|
||||
env: opts.envId,
|
||||
envtype: this.#envTypeToLabelValue(opts.envType),
|
||||
org: opts.orgId,
|
||||
project: opts.projectId,
|
||||
run: opts.runId,
|
||||
checkpoint: opts.checkpointId,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
restartPolicy: "Never",
|
||||
imagePullSecrets: [
|
||||
{
|
||||
name: "registry-trigger",
|
||||
},
|
||||
],
|
||||
nodeSelector: {
|
||||
nodetype: "worker",
|
||||
},
|
||||
initContainers: [
|
||||
{
|
||||
name: "pull-base-image",
|
||||
image: opts.imageRef,
|
||||
command: ["sleep", "0"],
|
||||
},
|
||||
{
|
||||
name: "populate-taskinfo",
|
||||
image: "busybox",
|
||||
command: ["/bin/sh", "-c"],
|
||||
args: ["printenv COORDINATOR_HOST | tee /etc/taskinfo/coordinator-host"],
|
||||
env: [
|
||||
{
|
||||
name: "COORDINATOR_HOST",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "status.hostIP",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "taskinfo",
|
||||
mountPath: "/etc/taskinfo",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
containers: [
|
||||
{
|
||||
name: this.#getRunContainerName(opts.runId),
|
||||
image: opts.checkpointRef,
|
||||
ports: [
|
||||
{
|
||||
containerPort: 8000,
|
||||
},
|
||||
],
|
||||
resources: {
|
||||
limits: this.#getResourcesFromMachineConfig(opts.machine),
|
||||
},
|
||||
lifecycle: {
|
||||
postStart: {
|
||||
exec: {
|
||||
command: this.#getLifecycleCommand("postStart", "restore"),
|
||||
},
|
||||
},
|
||||
preStop: {
|
||||
exec: {
|
||||
command: this.#getLifecycleCommand("preStop", "terminate"),
|
||||
},
|
||||
},
|
||||
},
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "taskinfo",
|
||||
mountPath: "/etc/taskinfo",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
volumes: [
|
||||
{
|
||||
name: "taskinfo",
|
||||
emptyDir: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
this.#namespace
|
||||
);
|
||||
}
|
||||
|
||||
async delete(opts: { runId: string }) {
|
||||
await this.#deletePod({
|
||||
runId: opts.runId,
|
||||
namespace: this.#namespace,
|
||||
});
|
||||
}
|
||||
|
||||
async get(opts: { runId: string }) {
|
||||
await this.#getPod(opts.runId, this.#namespace);
|
||||
}
|
||||
|
||||
#envTypeToLabelValue(type: EnvironmentType) {
|
||||
switch (type) {
|
||||
case "PRODUCTION":
|
||||
return "prod";
|
||||
case "STAGING":
|
||||
return "stg";
|
||||
case "DEVELOPMENT":
|
||||
return "dev";
|
||||
case "PREVIEW":
|
||||
return "preview";
|
||||
}
|
||||
}
|
||||
|
||||
#getResourcesFromMachineConfig(config: Machine) {
|
||||
return {
|
||||
cpu: `${config.cpu}`,
|
||||
memory: `${config.memory}G`,
|
||||
};
|
||||
}
|
||||
|
||||
#getLifecycleCommand<THookType extends "postStart" | "preStop">(
|
||||
type: THookType,
|
||||
cause: THookType extends "postStart" ? PostStartCauses : PreStopCauses
|
||||
) {
|
||||
return ["/bin/sh", "-c", `sleep 1; wget -q -O- 127.0.0.1:8000/${type}?cause=${cause}`];
|
||||
}
|
||||
|
||||
#getIndexContainerName(suffix: string) {
|
||||
return `task-index-${suffix}`;
|
||||
}
|
||||
|
||||
#getRunContainerName(suffix: string) {
|
||||
return `task-run-${suffix}`;
|
||||
}
|
||||
|
||||
#createK8sApi() {
|
||||
const kubeConfig = new k8s.KubeConfig();
|
||||
|
||||
if (RUNTIME_ENV === "local") {
|
||||
kubeConfig.loadFromDefault();
|
||||
} else if (RUNTIME_ENV === "kubernetes") {
|
||||
kubeConfig.loadFromCluster();
|
||||
} else {
|
||||
throw new Error(`Unsupported runtime environment: ${RUNTIME_ENV}`);
|
||||
}
|
||||
|
||||
return {
|
||||
core: kubeConfig.makeApiClient(k8s.CoreV1Api),
|
||||
batch: kubeConfig.makeApiClient(k8s.BatchV1Api),
|
||||
};
|
||||
}
|
||||
|
||||
async #createPod(pod: k8s.V1Pod, namespace: Namespace) {
|
||||
try {
|
||||
const res = await this.#k8sApi.core.createNamespacedPod(namespace.metadata.name, pod);
|
||||
logger.debug(res.body);
|
||||
} catch (err: unknown) {
|
||||
this.#handleK8sError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async #deletePod(opts: { runId: string; namespace: Namespace }) {
|
||||
try {
|
||||
const res = await this.#k8sApi.core.deleteNamespacedPod(
|
||||
opts.runId,
|
||||
opts.namespace.metadata.name
|
||||
);
|
||||
logger.debug(res.body);
|
||||
} catch (err: unknown) {
|
||||
this.#handleK8sError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async #getPod(runId: string, namespace: Namespace) {
|
||||
try {
|
||||
const res = await this.#k8sApi.core.readNamespacedPod(runId, namespace.metadata.name);
|
||||
logger.debug(res.body);
|
||||
return res.body;
|
||||
} catch (err: unknown) {
|
||||
this.#handleK8sError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async #createJob(job: k8s.V1Job, namespace: Namespace) {
|
||||
try {
|
||||
const res = await this.#k8sApi.batch.createNamespacedJob(namespace.metadata.name, job);
|
||||
logger.debug(res.body);
|
||||
} catch (err: unknown) {
|
||||
this.#handleK8sError(err);
|
||||
}
|
||||
}
|
||||
|
||||
#throwUnlessRecord(candidate: unknown): asserts candidate is Record<string, unknown> {
|
||||
if (typeof candidate !== "object" || candidate === null) {
|
||||
throw candidate;
|
||||
}
|
||||
}
|
||||
|
||||
#handleK8sError(err: unknown) {
|
||||
this.#throwUnlessRecord(err);
|
||||
|
||||
if ("body" in err && err.body) {
|
||||
logger.error(err.body);
|
||||
this.#throwUnlessRecord(err.body);
|
||||
|
||||
if (typeof err.body.message === "string") {
|
||||
throw new Error(err.body?.message);
|
||||
} else {
|
||||
throw err.body;
|
||||
}
|
||||
} else {
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new ProviderShell({
|
||||
tasks: new KubernetesTaskOperations(),
|
||||
type: "kubernetes",
|
||||
});
|
||||
|
||||
provider.listen();
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@trigger.dev/core/v3": ["../../packages/core/src/v3"],
|
||||
"@trigger.dev/core/v3/*": ["../../packages/core/src/v3/*"],
|
||||
"@trigger.dev/core-apps": ["../../packages/core-apps/src"],
|
||||
"@trigger.dev/core-apps/*": ["../../packages/core-apps/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
REWRITE_HOSTNAME=
|
||||
AWS_SQS_ACCESS_KEY_ID=
|
||||
AWS_SQS_SECRET_ACCESS_KEY=
|
||||
AWS_SQS_QUEUE_URL=
|
||||
AWS_SQS_REGION=
|
||||
#optional
|
||||
#REWRITE_PORT=
|
||||
@@ -0,0 +1,13 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
@@ -0,0 +1,172 @@
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
# wrangler project
|
||||
|
||||
.dev.vars
|
||||
.wrangler/
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# proxy
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/core@2.3.5
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/core@2.3.4
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/core@2.3.3
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/core@2.3.2
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [f3efcc0c]
|
||||
- @trigger.dev/core@2.3.1
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [17f6f29d]
|
||||
- @trigger.dev/core@2.3.0
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/core@2.2.11
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/core@2.2.10
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6ebd435e]
|
||||
- @trigger.dev/core@2.2.9
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [067e19fe]
|
||||
- @trigger.dev/core@2.2.8
|
||||
|
||||
## 0.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [756024da]
|
||||
- @trigger.dev/core@2.2.7
|
||||
@@ -0,0 +1,68 @@
|
||||
# Trigger.dev proxy
|
||||
|
||||
This is an optional module that can be used to proxy and queue requests to the Trigger.dev API.
|
||||
|
||||
## Why?
|
||||
|
||||
The Trigger.dev API is designed to be fast and reliable. However, if you have a lot of traffic, you may want to use this proxy to queue requests to the API. It intercepts some requests to the API and adds them to an AWS SQS queue, then the webapp can be setup to process the queue.
|
||||
|
||||
## Current features
|
||||
|
||||
- Intercepts `sendEvent` requests and adds them to an AWS SQS queue. The webapp then reads from the queue and creates the events.
|
||||
|
||||
## Setup
|
||||
|
||||
### Create an AWS SQS queue
|
||||
|
||||
In AWS you should create a new AWS SQS queue with appropriate security settings. You will need the queue URL for the next step.
|
||||
|
||||
### Environment variables
|
||||
|
||||
#### Cloudflare secrets
|
||||
|
||||
Locally you should copy the `.dev.var.example` file to `.dev.var` and fill in the values.
|
||||
|
||||
When deploying you should use `wrangler` (the Cloudflare CLI tool) to set secrets. Make sure you set the correct --env ("staging" or "prod")
|
||||
|
||||
```bash
|
||||
wrangler secret put REWRITE_HOSTNAME --env staging
|
||||
wrangler secret put AWS_SQS_ACCESS_KEY_ID --env staging
|
||||
wrangler secret put AWS_SQS_SECRET_ACCESS_KEY --env staging
|
||||
wrangler secret put AWS_SQS_QUEUE_URL --env staging
|
||||
wrangler secret put AWS_SQS_REGION --env staging
|
||||
```
|
||||
|
||||
You need to set your API CNAME entry to be proxied by Cloudflare. You can do this in the Cloudflare dashboard.
|
||||
|
||||
#### Webapp
|
||||
|
||||
These env vars also need setting in the webapp.
|
||||
|
||||
```bash
|
||||
AWS_SQS_REGION
|
||||
AWS_SQS_ACCESS_KEY_ID
|
||||
AWS_SQS_SECRET_ACCESS_KEY
|
||||
AWS_SQS_QUEUE_URL
|
||||
AWS_SQS_BATCH_SIZE
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Staging:
|
||||
|
||||
```bash
|
||||
npx wrangler@latest deploy --route "<your-api-subdomain>/*" --env staging
|
||||
```
|
||||
|
||||
Prod:
|
||||
|
||||
```bash
|
||||
npx wrangler@latest deploy --route "<your-api-subdomain>/*" --env prod
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Set the environment variables as described above.
|
||||
|
||||
1. `pnpm install`
|
||||
2. `pnpm run dev --filter proxy`
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "proxy",
|
||||
"version": "0.0.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20230419.0",
|
||||
"typescript": "^5.0.4",
|
||||
"wrangler": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sqs": "^3.445.0",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"ulidx": "^2.2.1",
|
||||
"zod": "3.22.3",
|
||||
"zod-error": "1.5.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const AuthorizationHeaderSchema = z.string().regex(/^Bearer .+$/);
|
||||
|
||||
export function getApiKeyFromRequest(request: Request) {
|
||||
const rawAuthorization = request.headers.get("Authorization");
|
||||
|
||||
const authorization = AuthorizationHeaderSchema.safeParse(rawAuthorization);
|
||||
if (!authorization.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = authorization.data.replace(/^Bearer /, "");
|
||||
const type = isPrivateApiKey(apiKey) ? ("PRIVATE" as const) : ("PUBLIC" as const);
|
||||
return { apiKey, type };
|
||||
}
|
||||
|
||||
function isPrivateApiKey(key: string) {
|
||||
return key.startsWith("tr_");
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
|
||||
import { ApiEventLog, SendEventBodySchema } from "@trigger.dev/core";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
import { Env } from "..";
|
||||
import { getApiKeyFromRequest } from "../apikey";
|
||||
import { json } from "../json";
|
||||
import { calculateDeliverAt } from "./utils";
|
||||
|
||||
/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */
|
||||
export async function queueEvent(request: Request, env: Env): Promise<Response> {
|
||||
//check there's a private API key
|
||||
const apiKeyResult = getApiKeyFromRequest(request);
|
||||
if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") {
|
||||
return json(
|
||||
{ error: "Invalid or Missing API key" },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//parse the request body
|
||||
try {
|
||||
const anyBody = await request.json();
|
||||
const body = SendEventBodySchema.safeParse(anyBody);
|
||||
if (!body.success) {
|
||||
return json(
|
||||
{ error: generateErrorMessage(body.error.issues) },
|
||||
{
|
||||
status: 422,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// The AWS SDK tries to use crypto from off of the window,
|
||||
// so we need to trick it into finding it where it expects it
|
||||
globalThis.global = globalThis;
|
||||
|
||||
const client = new SQSClient({
|
||||
region: env.AWS_SQS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
const timestamp = body.data.event.timestamp ?? new Date();
|
||||
|
||||
//add the event to the queue
|
||||
const send = new SendMessageCommand({
|
||||
// use wrangler secrets to provide this global variable
|
||||
QueueUrl: env.AWS_SQS_QUEUE_URL,
|
||||
MessageBody: JSON.stringify({
|
||||
event: { ...body.data.event, timestamp },
|
||||
options: body.data.options,
|
||||
apiKey: apiKeyResult.apiKey,
|
||||
}),
|
||||
});
|
||||
|
||||
const queuedEvent = await client.send(send);
|
||||
console.log("Queued event", queuedEvent);
|
||||
|
||||
//respond with the event
|
||||
const event: ApiEventLog = {
|
||||
id: body.data.event.id,
|
||||
name: body.data.event.name,
|
||||
payload: body.data.event.payload,
|
||||
context: body.data.event.context,
|
||||
timestamp,
|
||||
deliverAt: calculateDeliverAt(body.data.options),
|
||||
};
|
||||
|
||||
return json(event, {
|
||||
status: 200,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("queueEvent error", e);
|
||||
return json(
|
||||
{
|
||||
error: `Failed to send event: ${e instanceof Error ? e.message : JSON.stringify(e)}`,
|
||||
},
|
||||
{
|
||||
status: 422,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { SQSClient, SendMessageBatchCommand } from "@aws-sdk/client-sqs";
|
||||
import { ApiEventLog, SendBulkEventsBodySchema } from "@trigger.dev/core";
|
||||
import { generateErrorMessage } from "zod-error";
|
||||
import { Env } from "..";
|
||||
import { getApiKeyFromRequest } from "../apikey";
|
||||
import { json } from "../json";
|
||||
import { calculateDeliverAt } from "./utils";
|
||||
|
||||
/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */
|
||||
export async function queueEvents(request: Request, env: Env): Promise<Response> {
|
||||
//check there's a private API key
|
||||
const apiKeyResult = getApiKeyFromRequest(request);
|
||||
if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") {
|
||||
return json(
|
||||
{ error: "Invalid or Missing API key" },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//parse the request body
|
||||
try {
|
||||
const anyBody = await request.json();
|
||||
const body = SendBulkEventsBodySchema.safeParse(anyBody);
|
||||
if (!body.success) {
|
||||
return json(
|
||||
{ error: generateErrorMessage(body.error.issues) },
|
||||
{
|
||||
status: 422,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// The AWS SDK tries to use crypto from off of the window,
|
||||
// so we need to trick it into finding it where it expects it
|
||||
globalThis.global = globalThis;
|
||||
|
||||
const client = new SQSClient({
|
||||
region: env.AWS_SQS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: env.AWS_SQS_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedEvents: ApiEventLog[] = body.data.events.map((event) => {
|
||||
const timestamp = event.timestamp ?? new Date();
|
||||
return {
|
||||
...event,
|
||||
payload: event.payload,
|
||||
timestamp,
|
||||
};
|
||||
});
|
||||
|
||||
//divide updatedEvents into multiple batches of 10 (max size SQS accepts)
|
||||
const batches: ApiEventLog[][] = [];
|
||||
let currentBatch: ApiEventLog[] = [];
|
||||
for (let i = 0; i < updatedEvents.length; i++) {
|
||||
currentBatch.push(updatedEvents[i]);
|
||||
if (currentBatch.length === 10) {
|
||||
batches.push(currentBatch);
|
||||
currentBatch = [];
|
||||
}
|
||||
}
|
||||
if (currentBatch.length > 0) {
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
|
||||
//loop through the batches and send them
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i];
|
||||
//add the event to the queue
|
||||
const send = new SendMessageBatchCommand({
|
||||
// use wrangler secrets to provide this global variable
|
||||
QueueUrl: env.AWS_SQS_QUEUE_URL,
|
||||
Entries: batch.map((event, index) => ({
|
||||
Id: `event-${index}`,
|
||||
MessageBody: JSON.stringify({
|
||||
event,
|
||||
options: body.data.options,
|
||||
apiKey: apiKeyResult.apiKey,
|
||||
}),
|
||||
})),
|
||||
});
|
||||
|
||||
const queuedEvent = await client.send(send);
|
||||
console.log("Queued events", queuedEvent);
|
||||
}
|
||||
|
||||
//respond with the events
|
||||
const events: ApiEventLog[] = updatedEvents.map((event) => ({
|
||||
...event,
|
||||
payload: event.payload,
|
||||
deliverAt: calculateDeliverAt(body.data.options),
|
||||
}));
|
||||
|
||||
return json(events, {
|
||||
status: 200,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("queueEvents error", e);
|
||||
return json(
|
||||
{
|
||||
error: `Failed to send events: ${e instanceof Error ? e.message : JSON.stringify(e)}`,
|
||||
},
|
||||
{
|
||||
status: 422,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SendEventOptions } from "@trigger.dev/core";
|
||||
|
||||
export function calculateDeliverAt(options?: SendEventOptions) {
|
||||
// If deliverAt is a string and a valid date, convert it to a Date object
|
||||
if (options?.deliverAt) {
|
||||
return options?.deliverAt;
|
||||
}
|
||||
|
||||
// deliverAfter is the number of seconds to wait before delivering the event
|
||||
if (options?.deliverAfter) {
|
||||
return new Date(Date.now() + options.deliverAfter * 1000);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { queueEvent } from "./events/queueEvent";
|
||||
import { queueEvents } from "./events/queueEvents";
|
||||
|
||||
export interface Env {
|
||||
/** The hostname needs to be changed to allow requests to pass to the Trigger.dev platform */
|
||||
REWRITE_HOSTNAME: string;
|
||||
REWRITE_PORT?: string;
|
||||
AWS_SQS_ACCESS_KEY_ID: string;
|
||||
AWS_SQS_SECRET_ACCESS_KEY: string;
|
||||
AWS_SQS_QUEUE_URL: string;
|
||||
AWS_SQS_REGION: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
if (!env.REWRITE_HOSTNAME) throw new Error("Missing REWRITE_HOSTNAME");
|
||||
console.log("url", request.url);
|
||||
|
||||
if (!queueingIsEnabled(env)) {
|
||||
console.log("Missing AWS credentials. Passing through to the origin.");
|
||||
return redirectToOrigin(request, env);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
switch (url.pathname) {
|
||||
case "/api/v1/events": {
|
||||
if (request.method === "POST") {
|
||||
return queueEvent(request, env);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "/api/v1/events/bulk": {
|
||||
if (request.method === "POST") {
|
||||
return queueEvents(request, env);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//the same request but with the hostname (and port) changed
|
||||
return redirectToOrigin(request, env);
|
||||
},
|
||||
};
|
||||
|
||||
function redirectToOrigin(request: Request, env: Env) {
|
||||
const newUrl = new URL(request.url);
|
||||
newUrl.hostname = env.REWRITE_HOSTNAME;
|
||||
newUrl.port = env.REWRITE_PORT || newUrl.port;
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
};
|
||||
|
||||
console.log("rewritten url", newUrl.toString());
|
||||
return fetch(newUrl.toString(), requestInit);
|
||||
}
|
||||
|
||||
function queueingIsEnabled(env: Env) {
|
||||
return (
|
||||
env.AWS_SQS_ACCESS_KEY_ID &&
|
||||
env.AWS_SQS_SECRET_ACCESS_KEY &&
|
||||
env.AWS_SQS_QUEUE_URL &&
|
||||
env.AWS_SQS_REGION
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export function json(body: any, init?: ResponseInit) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
};
|
||||
|
||||
const responseInit: ResponseInit = {
|
||||
...(init ?? {}),
|
||||
headers,
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(body), responseInit);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"lib": [
|
||||
"es2021"
|
||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||
|
||||
"module": "es2022" /* Specify what module code is generated. */,
|
||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
|
||||
"types": [
|
||||
"@cloudflare/workers-types"
|
||||
] /* Specify type package names to be included without being referenced in a source file. */,
|
||||
"resolveJsonModule": true /* Enable importing .json files */,
|
||||
|
||||
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
|
||||
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
|
||||
|
||||
"noEmit": true /* Disable emitting files from a compilation. */,
|
||||
|
||||
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
|
||||
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@trigger.dev/core": ["../../packages/core/src/index"],
|
||||
"@trigger.dev/core/*": ["../../packages/core/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
name = "proxy"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2023-10-30"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
[env.staging]
|
||||
[env.prod]
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { StorybookConfig } from "@storybook/react-webpack5";
|
||||
import path from "path";
|
||||
|
||||
const root = path.resolve(__dirname, "../app");
|
||||
|
||||
const config: StorybookConfig = {
|
||||
webpackFinal: async (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...(config.resolve?.alias ?? {}),
|
||||
"~": root,
|
||||
},
|
||||
extensions: [
|
||||
...(config.resolve?.extensions ?? []),
|
||||
...[".ts", ".tsx", ".js", ".jsx", ".mdx"],
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
stories: ["../app/**/stories/*.mdx", "../app/**/stories/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-addon-variants",
|
||||
"storybook-addon-designs",
|
||||
"@storybook/addon-docs",
|
||||
{
|
||||
name: "@storybook/addon-styling",
|
||||
options: {
|
||||
// Check out https://github.com/storybookjs/addon-styling/blob/main/docs/api.md
|
||||
// For more details on this addon's options.
|
||||
postCss: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-webpack5",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
staticDirs: [path.resolve("public")],
|
||||
};
|
||||
export default config;
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../app/tailwind.css";
|
||||
import { createRemixStub } from "@remix-run/testing";
|
||||
import React from "react";
|
||||
import { LocaleContextProvider } from "../app/components/primitives/LocaleProvider";
|
||||
import { OperatingSystemContextProvider } from "../app/components/primitives/OperatingSystemProvider";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
backgrounds: {
|
||||
default: "App background",
|
||||
values: [
|
||||
{
|
||||
name: "App background",
|
||||
value: "#0B1018",
|
||||
},
|
||||
],
|
||||
},
|
||||
layout: "fullscreen",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const RemixStub = createRemixStub([
|
||||
{
|
||||
path: "/*",
|
||||
action: () => ({ redirect: "/" }),
|
||||
loader: () => ({ redirect: "/" }),
|
||||
Component: Story,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<OperatingSystemContextProvider platform="mac">
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixStub initialEntries={["/"]} />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,29 @@
|
||||
export function AttemptIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_9964_113464)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 0H0V16H16V0ZM7.09906 4.4L4.53906 11.5H6.11906L6.63906 10H9.35906L9.87906 11.5H11.4591L8.89906 4.4H7.09906ZM7.99906 6L8.92906 8.73H7.06906L7.99906 6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9964_113464">
|
||||
<path
|
||||
d="M0 2C0 0.895431 0.895431 0 2 0H14C15.1046 0 16 0.895431 16 2V14C16 15.1046 15.1046 16 14 16H2C0.895431 16 0 15.1046 0 14V2Z"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export function ExitIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="3.5" y1="8" x2="11.5" y2="8" stroke="currentColor" strokeLinecap="round" />
|
||||
<line x1="15.5" y1="1.5" x2="15.5" y2="14.5" stroke="currentColor" strokeLinecap="round" />
|
||||
<path
|
||||
d="M8.5 4.5L12 8L8.5 11.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export function ShowParentIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.125 16H3.625H4.125V15H3.125H2C1.89733 15 1.8 14.9848 1.70919 14.9573L1.19765 14.8024L1.04267 14.2908C1.01515 14.2 1 14.1027 1 14V12.875V11.875H0V12.375V12.875V14C0 14.202 0.0299409 14.397 0.0856265 14.5808C0.135047 14.7439 0.204744 14.8982 0.291786 15.0407C0.363386 15.158 0.446722 15.2673 0.540161 15.3671C0.570065 15.399 0.601004 15.4299 0.632924 15.4598C0.732663 15.5533 0.841982 15.6366 0.95925 15.7082C1.10181 15.7953 1.25612 15.865 1.41923 15.9144C1.60303 15.9701 1.79802 16 2 16H3.125ZM10.125 15V16H9.625H9.125H6.875H6.375H5.875V15H6.875H9.125H10.125ZM15 5.875H16V6.375V6.875V9.125V9.625V10.125H15V9.125V6.875V5.875ZM5.875 1V0H6.375H6.875H9.125H9.625H10.125V1H9.125H6.875H5.875ZM1 10.125H0V9.625V9.125V6.875V6.375V5.875H1V6.875V9.125V10.125ZM0 3.625V4.125H1V3.125V2C1 1.89733 1.01515 1.8 1.04267 1.70919L1.19765 1.19765L1.70919 1.04267C1.8 1.01515 1.89733 1 2 1H3.125H4.125V0H3.625H3.125H2C1.79802 0 1.60303 0.0299409 1.41923 0.0856264C1.25612 0.135047 1.10181 0.204744 0.95925 0.291786C0.841983 0.363386 0.732663 0.446722 0.632924 0.540161C0.601004 0.570065 0.570065 0.601004 0.540161 0.632924C0.446722 0.732663 0.363386 0.841982 0.291786 0.95925C0.204744 1.10181 0.135047 1.25612 0.0856265 1.41923C0.0299409 1.60303 0 1.79802 0 2V3.125V3.625ZM12.375 0H11.875V1H12.875H14C14.1027 1 14.2 1.01515 14.2908 1.04267L14.8024 1.19765L14.9573 1.70919C14.9848 1.8 15 1.89733 15 2V3.125V4.125H16V3.625V3.125V2C16 1.79802 15.9701 1.60303 15.9144 1.41923C15.865 1.25612 15.7953 1.10181 15.7082 0.95925C15.6366 0.841982 15.5533 0.732663 15.4598 0.632924C15.4299 0.601004 15.399 0.570065 15.3671 0.540161C15.2673 0.446722 15.158 0.363386 15.0407 0.291786C14.8982 0.204744 14.7439 0.135047 14.5808 0.0856265C14.397 0.0299409 14.202 0 14 0H12.875H12.375ZM16 12.375V11.875H15V12.875V14C15 14.1027 14.9848 14.2 14.9573 14.2908L14.8024 14.8024L14.2908 14.9573C14.2 14.9848 14.1027 15 14 15H12.875H11.875V16H12.375H12.875H14C14.202 16 14.397 15.9701 14.5808 15.9144C14.7439 15.865 14.8982 15.7953 15.0407 15.7082C15.158 15.6366 15.2673 15.5533 15.3671 15.4598C15.399 15.4299 15.4299 15.399 15.4598 15.3671C15.5533 15.2673 15.6366 15.158 15.7082 15.0407C15.7953 14.8982 15.865 14.7439 15.9144 14.5808C15.9701 14.397 16 14.202 16 14V12.875V12.375ZM8.75 6.26758L10.4523 7.96991C10.7452 8.26281 11.2201 8.26281 11.513 7.96991C11.8059 7.67702 11.8059 7.20215 11.513 6.90925L8.59099 3.98725C8.43431 3.83057 8.22556 3.7577 8.02045 3.76865C7.81604 3.75834 7.60821 3.83124 7.45209 3.98736L4.53009 6.90937C4.23719 7.20226 4.23719 7.67713 4.53009 7.97003C4.82298 8.26292 5.29785 8.26292 5.59075 7.97003L7.25 6.31077L7.25 11.25C7.25 11.6642 7.58579 12 8 12C8.41421 12 8.75 11.6642 8.75 11.25L8.75 6.26758Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShowParentIconSelected({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 0C0.895431 0 0 0.895431 0 2V14C0 15.1046 0.895431 16 2 16H14C15.1046 16 16 15.1046 16 14V2C16 0.895431 15.1046 0 14 0H2ZM8.75 6.26758L10.4523 7.96991C10.7452 8.26281 11.2201 8.26281 11.513 7.96991C11.8059 7.67702 11.8059 7.20215 11.513 6.90925L8.59099 3.98725C8.43344 3.8297 8.22324 3.7569 8.01703 3.76884C7.8138 3.75958 7.60752 3.83254 7.45234 3.98773L4.53033 6.90973C4.23744 7.20263 4.23744 7.6775 4.53033 7.97039C4.82322 8.26329 5.2981 8.26329 5.59099 7.97039L7.25 6.31138L7.25 11.25C7.25 11.6642 7.58579 12 8 12C8.41421 12 8.75 11.6642 8.75 11.25L8.75 6.26758Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export function TaskIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_9221_99732)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 0H0V16H16V0ZM10.8901 5.73995V4.44995H5.11011V5.73995H7.23011V11.55H8.77011V5.73995H10.8901Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9221_99732">
|
||||
<path
|
||||
d="M0 2C0 0.895431 0.895431 0 2 0H14C15.1046 0 16 0.895431 16 2V14C16 15.1046 15.1046 16 14 16H2C0.895431 16 0 15.1046 0 14V2Z"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 166 B |
Binary file not shown.
|
Before Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 170 B |
@@ -0,0 +1,21 @@
|
||||
export function ATAndTLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="66"
|
||||
height="33"
|
||||
viewBox="0 0 66 33"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M57.0405 21.156C57.0064 21.1581 56.9723 21.1529 56.9403 21.1408C56.9084 21.1287 56.8794 21.1099 56.8552 21.0858C56.8311 21.0616 56.8123 21.0326 56.8002 21.0007C56.7881 20.9687 56.7829 20.9346 56.785 20.9005V13.4102H54.2407C54.2068 13.4114 54.173 13.4056 54.1415 13.3932C54.1099 13.3808 54.0813 13.362 54.0573 13.338C54.0333 13.3141 54.0145 13.2854 54.0021 13.2538C53.9897 13.2223 53.984 13.1885 53.9852 13.1546V12.1226C53.9839 12.0887 53.9896 12.0549 54.002 12.0233C54.0143 11.9916 54.0331 11.9629 54.0571 11.9389C54.0811 11.9148 54.1098 11.896 54.1414 11.8836C54.173 11.8711 54.2068 11.8653 54.2407 11.8666H61.0127C61.0468 11.8646 61.0809 11.8698 61.1129 11.882C61.1448 11.8942 61.1738 11.913 61.1979 11.9372C61.222 11.9614 61.2407 11.9904 61.2527 12.0224C61.2647 12.0544 61.2698 12.0886 61.2677 12.1226V13.1557C61.2689 13.1896 61.2632 13.2233 61.2508 13.2548C61.2384 13.2863 61.2197 13.315 61.1958 13.3389C61.1718 13.3629 61.1432 13.3817 61.1118 13.3941C61.0803 13.4066 61.0465 13.4124 61.0127 13.4112H58.4684V20.9005C58.4696 20.9344 58.4639 20.9681 58.4515 20.9997C58.4391 21.0313 58.4203 21.0599 58.3963 21.0839C58.3723 21.1079 58.3437 21.1266 58.3121 21.1391C58.2805 21.1515 58.2468 21.1572 58.2129 21.156H57.0394M37.5459 17.4163L36.2102 13.5845L34.8629 17.4163H37.5459ZM40.5081 20.8528C40.5662 21.0041 40.4735 21.1555 40.3107 21.1555H39.1087C39.0327 21.1609 38.9572 21.1394 38.8955 21.0947C38.8338 21.0499 38.7899 20.9848 38.7715 20.9109L38.0861 18.9369H34.3343L33.6489 20.9109C33.6305 20.9847 33.5867 21.0498 33.5251 21.0945C33.4635 21.1392 33.3881 21.1608 33.3122 21.1555H32.1744C32.0231 21.1555 31.9183 21.0041 31.977 20.8528L35.1245 12.0985C35.1826 11.9357 35.2873 11.866 35.4612 11.866H37.0184C37.1928 11.866 37.3091 11.9351 37.3672 12.0985L40.5147 20.8528M49.5381 19.9014C50.2811 19.9014 50.7812 19.5422 51.1875 18.9265L49.3067 16.9058C48.5862 17.3127 48.1212 17.7185 48.1212 18.5316C48.1212 19.3322 48.7716 19.9025 49.5387 19.9025M50.0613 13.1031C49.4581 13.1031 49.1088 13.4869 49.1088 13.9969C49.1088 14.3917 49.3172 14.7399 49.7942 15.2509C50.6189 14.7739 50.9677 14.4843 50.9677 13.9733C50.9677 13.4962 50.6661 13.096 50.0618 13.096M55.2025 20.8303C55.3533 20.9931 55.2606 21.1555 55.0742 21.1555H53.5937C53.511 21.1627 53.4278 21.1483 53.3524 21.1136C53.277 21.0788 53.212 21.025 53.1638 20.9575L52.2864 19.9826C51.6942 20.7722 50.869 21.3644 49.4981 21.3644C47.8021 21.3644 46.4658 20.3428 46.4658 18.5898C46.4658 17.2425 47.1863 16.5225 48.2786 15.9194C47.744 15.3041 47.5 14.6538 47.5 14.0852C47.5 12.6447 48.5106 11.6582 50.0316 11.6582C51.5889 11.6582 52.5408 12.5761 52.5408 13.9338C52.5408 15.0952 51.7046 15.7444 50.8218 16.2325L52.123 17.6379L52.8545 16.3602C52.9477 16.2094 53.0519 16.1519 53.2383 16.1519H54.3646C54.5511 16.1519 54.6553 16.2802 54.5401 16.477L53.2449 18.706L55.208 20.8314M43.6161 21.1566C43.65 21.1579 43.6838 21.1521 43.7155 21.1398C43.7471 21.1274 43.7758 21.1086 43.7998 21.0846C43.8239 21.0607 43.8427 21.032 43.8552 21.0004C43.8676 20.9688 43.8734 20.935 43.8721 20.901V13.4107H46.4164C46.4503 13.4119 46.4841 13.4062 46.5157 13.3938C46.5472 13.3813 46.5759 13.3626 46.5999 13.3386C46.6238 13.3146 46.6426 13.2859 46.655 13.2544C46.6674 13.2228 46.6732 13.1891 46.672 13.1552V12.1226C46.6733 12.0887 46.6676 12.0549 46.6552 12.0233C46.6428 11.9916 46.624 11.9629 46.6001 11.9389C46.5761 11.9148 46.5474 11.896 46.5158 11.8836C46.4842 11.8711 46.4504 11.8653 46.4164 11.8666H39.6406C39.6067 11.8653 39.5728 11.8711 39.5412 11.8836C39.5097 11.896 39.481 11.9148 39.457 11.9389C39.433 11.9629 39.4142 11.9916 39.4019 12.0233C39.3895 12.0549 39.3838 12.0887 39.3851 12.1226V13.1557C39.3838 13.1896 39.3896 13.2234 39.402 13.2549C39.4144 13.2865 39.4332 13.3152 39.4572 13.3391C39.4812 13.3631 39.5098 13.3819 39.5414 13.3943C39.5729 13.4067 39.6067 13.4125 39.6406 13.4112H42.1849V20.9005C42.1837 20.9344 42.1894 20.9681 42.2018 20.9997C42.2143 21.0313 42.233 21.0599 42.257 21.0839C42.281 21.1079 42.3096 21.1266 42.3412 21.1391C42.3728 21.1515 42.4065 21.1572 42.4404 21.156L43.6161 21.1566Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.2351 25.6737C11.2728 27.2567 13.7799 28.1157 16.3602 28.1149C19.296 28.1149 21.9725 27.0248 24.0151 25.2361C24.0397 25.2142 24.0277 25.1999 24.003 25.2142C23.0862 25.8261 20.4739 27.1624 16.3602 27.1624C12.7851 27.1624 10.5259 26.3646 9.2499 25.6529C9.22523 25.6408 9.217 25.6583 9.23455 25.6748M17.1487 26.2686C20.0083 26.2686 23.1503 25.4889 25.0295 23.9464C25.5438 23.5258 26.033 22.9665 26.4716 22.2148C26.74 21.7483 26.9742 21.263 27.1724 20.7628C27.1812 20.7381 27.1669 20.726 27.1477 20.754C25.4002 23.3306 20.339 24.9296 15.1188 24.9296C11.4252 24.9296 7.45135 23.7485 5.89571 21.4931C5.88035 21.4723 5.865 21.4811 5.87377 21.5052C7.3181 24.5863 11.7152 26.2686 17.1476 26.2686M14.0232 21.1581C8.07591 21.1581 5.2717 18.389 4.76338 16.4983C4.7579 16.4709 4.73926 16.4764 4.73926 16.5016C4.73926 17.1377 4.80287 17.9591 4.91253 18.5041C4.96463 18.7695 5.1867 19.1857 5.49706 19.5186C6.9392 21.0166 10.5286 23.1212 16.7468 23.1212C25.2187 23.1212 27.1554 20.2994 27.5508 19.3705C27.8337 18.7125 27.9801 17.5073 27.9801 16.5C27.9803 16.29 27.9751 16.0801 27.9648 15.8705C27.9648 15.8392 27.9467 15.8376 27.9406 15.8672C27.5173 18.1373 20.2792 21.157 14.0249 21.157M5.85458 11.5172C5.49236 12.2876 5.21385 13.0946 5.02385 13.9244C4.96901 14.1766 4.99533 14.2989 5.07868 14.4876C5.79152 15.9999 9.39686 18.4192 17.8068 18.4192C22.9376 18.4192 26.9235 17.158 27.5689 14.8582C27.6878 14.4349 27.6939 13.9875 27.5414 13.3854C27.3769 12.712 27.0512 11.9268 26.7803 11.3757C26.7716 11.3576 26.7557 11.3604 26.7584 11.3812C26.8588 14.3982 18.4456 16.3426 14.2003 16.3426C9.60194 16.3426 5.76685 14.5111 5.76685 12.1971C5.76685 11.9751 5.81291 11.7585 5.87652 11.521C5.882 11.4991 5.86445 11.4964 5.85458 11.5156M24.0348 7.81261C24.0859 7.89265 24.1114 7.9864 24.1077 8.0813C24.1077 9.37209 20.158 11.6548 13.8702 11.6548C9.25045 11.6548 8.38572 9.94127 8.38572 8.85117C8.38572 8.46733 8.53487 8.0632 8.86442 7.65798C8.88252 7.63385 8.86716 7.62508 8.84633 7.64262C8.24498 8.15186 7.6971 8.72105 7.21118 9.34138C6.97978 9.63365 6.83666 9.89246 6.83666 10.0476C6.83666 12.3068 12.4999 13.9441 17.7958 13.9441C23.4437 13.9441 25.9562 12.1017 25.9562 10.4896C25.9562 9.91111 25.7369 9.57388 25.1556 8.91861C24.7816 8.49255 24.428 8.15094 24.0589 7.80438C24.0408 7.78958 24.0282 7.80164 24.0408 7.81974M22.3108 6.52949C20.5693 5.48545 18.547 4.8916 16.3668 4.8916C14.1713 4.8916 12.0887 5.50574 10.3351 6.57775C9.81086 6.90017 9.51585 7.15899 9.51585 7.49128C9.51585 8.47117 11.8057 9.52453 15.8673 9.52453C19.8866 9.52453 23.005 8.37082 23.005 7.25988C23.005 6.99503 22.7731 6.80915 22.3042 6.52949"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
|
||||
export function CalComLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="64"
|
||||
height="15"
|
||||
viewBox="0 0 64 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_9632_116886)">
|
||||
<path
|
||||
d="M6.38629 13.7111C2.75645 13.7111 0.0224609 10.8382 0.0224609 7.29138C0.0224609 3.73276 2.61685 0.836182 6.38629 0.836182C8.38733 0.836182 9.77174 1.45096 10.8537 2.85787L9.1086 4.31207C8.37568 3.53177 7.49149 3.14162 6.38629 3.14162C3.93149 3.14162 2.58194 5.02143 2.58194 7.29138C2.58194 9.56139 4.05946 11.4057 6.38629 11.4057C7.47984 11.4057 8.41061 11.0156 9.14353 10.2353L10.8654 11.7486C9.82995 13.0963 8.41061 13.7111 6.38629 13.7111Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M18.3806 4.26438H20.7307V13.4861H18.3806V12.1383C17.892 13.096 17.0776 13.7344 15.5187 13.7344C13.029 13.7344 11.0396 11.5709 11.0396 8.91074C11.0396 6.2506 13.029 4.08704 15.5187 4.08704C17.066 4.08704 17.892 4.72547 18.3806 5.68311V4.26438ZM18.4504 8.91074C18.4504 7.46837 17.4615 6.27425 15.9026 6.27425C14.4018 6.27425 13.4245 7.48014 13.4245 8.91074C13.4245 10.3058 14.4018 11.5472 15.9026 11.5472C17.4499 11.5472 18.4504 10.3413 18.4504 8.91074Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M22.395 0.540283H24.7451V13.4744H22.395V0.540283Z" fill="currentColor" />
|
||||
<path
|
||||
d="M25.791 12.2569C25.791 11.5003 26.396 10.8618 27.222 10.8618C28.048 10.8618 28.6298 11.5003 28.6298 12.2569C28.6298 13.0372 28.0364 13.6756 27.222 13.6756C26.4076 13.6756 25.791 13.0372 25.791 12.2569Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M37.6222 11.9965C36.7496 13.0605 35.4234 13.7344 33.8527 13.7344C31.0489 13.7344 28.9897 11.5709 28.9897 8.91074C28.9897 6.2506 31.0489 4.08704 33.8527 4.08704C35.3651 4.08704 36.6798 4.72547 37.5524 5.7304L35.7375 7.25553C35.2837 6.68804 34.6904 6.26242 33.8527 6.26242C32.352 6.26242 31.3747 7.46837 31.3747 8.8989C31.3747 10.3294 32.352 11.5354 33.8527 11.5354C34.7602 11.5354 35.3884 11.0625 35.8538 10.424L37.6222 11.9965Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M37.8208 8.91074C37.8208 6.2506 39.88 4.08704 42.6838 4.08704C45.4876 4.08704 47.5468 6.2506 47.5468 8.91074C47.5468 11.5709 45.4876 13.7344 42.6838 13.7344C39.88 13.7225 37.8208 11.5709 37.8208 8.91074ZM45.1618 8.91074C45.1618 7.46837 44.1846 6.27425 42.6838 6.27425C41.183 6.26242 40.2058 7.46837 40.2058 8.91074C40.2058 10.3413 41.183 11.5472 42.6838 11.5472C44.1846 11.5472 45.1618 10.3413 45.1618 8.91074Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M63.4389 7.84684V13.4745H61.089V8.42614C61.089 6.83009 60.3444 6.14437 59.2276 6.14437C58.1805 6.14437 57.4359 6.66457 57.4359 8.42614V13.4745H55.0859V8.42614C55.0859 6.83009 54.3296 6.14437 53.2244 6.14437C52.1774 6.14437 51.2582 6.66457 51.2582 8.42614V13.4745H48.9082V4.25273H51.2582V5.52958C51.7469 4.53647 52.6311 4.03992 53.9923 4.03992C55.2837 4.03992 56.3656 4.67834 56.9589 5.75421C57.5523 4.6547 58.4248 4.03992 60.007 4.03992C61.9383 4.05174 63.4389 5.51776 63.4389 7.84684Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9632_116886">
|
||||
<rect
|
||||
width="63.9027"
|
||||
height="13.9194"
|
||||
fill="currentColor"
|
||||
transform="translate(0.0224609 0.540283)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
export function ShopifyLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="85"
|
||||
height="25"
|
||||
viewBox="0 0 85 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_9632_116870)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.6613 1.00225C10.5632 0.945637 10.4859 0.900988 10.3639 0.900988C8.19769 0.900988 7.03127 3.73373 6.44806 5.40004L8.28101 4.90015C8.7809 2.5673 9.78069 1.4842 10.7805 1.06762C10.7358 1.0453 10.6972 1.02297 10.6613 1.00225ZM11.6136 3.73373C11.6136 2.81725 11.447 2.23404 11.2804 1.73415C10.5305 2.06741 9.69738 2.81725 9.19748 4.56688L11.6136 3.90036V3.73373ZM12.3635 3.56709L13.4466 3.23383C13.1966 2.40067 12.6967 1.65083 12.1135 1.4842C12.2802 1.98409 12.3635 2.5673 12.3635 3.31715V3.56709ZM18.6955 4.40025C18.7788 4.40025 18.8621 4.48357 18.8621 4.56688C18.8621 4.73352 21.3616 21.7299 21.3616 21.7299L14.1964 23.3129L0.949219 20.8135C0.949219 20.8135 2.61553 7.64957 2.69885 7.23299C2.70036 7.22241 2.70184 7.212 2.7033 7.20175C2.78239 6.64673 2.79425 6.56344 3.44869 6.39983C3.64023 6.33599 3.82633 6.27214 4.00838 6.20968C4.53904 6.02763 5.03532 5.85737 5.53159 5.7333C6.03148 3.81704 7.44785 0.0678302 10.4472 0.151146C10.8638 0.151146 11.3637 0.317778 11.697 0.817672H11.7803C13.03 0.817672 13.7799 1.90078 14.1964 3.0672C14.613 2.90057 14.863 2.81725 14.863 2.81725C14.8796 2.81725 14.9029 2.81392 14.9309 2.80992C15.0429 2.79392 15.2295 2.76726 15.3629 2.90057C15.5295 2.98388 16.7792 4.23362 16.7792 4.23362C16.7792 4.23362 18.5289 4.40025 18.6955 4.40025ZM10.8622 11.0656L11.7786 8.39946C11.7786 8.39946 11.1954 7.98288 9.77906 8.06619C6.69638 8.06619 5.11338 10.1491 5.11338 12.3153C5.11338 13.764 5.92592 14.4263 6.63951 15.008C7.19816 15.4633 7.69617 15.8693 7.69617 16.5644C7.69617 16.981 7.44622 17.4809 6.7797 17.4809C5.77991 17.4809 4.61348 16.4811 4.61348 16.4811L4.03027 18.4807C4.03027 18.4807 5.19669 19.897 7.44622 19.897C9.36249 19.897 10.7789 18.4807 10.7789 16.2311C10.7789 14.5056 9.60415 13.6457 8.68583 12.9735C8.09656 12.5421 7.61285 12.188 7.61285 11.7321C7.61285 11.4821 7.69617 10.649 9.11254 10.649C10.1123 10.649 10.8622 11.0656 10.8622 11.0656Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M18.6111 4.40038C18.5277 4.40038 16.7781 4.23375 16.7781 4.23375C16.7781 4.23375 15.5284 2.98401 15.3617 2.9007C15.2784 2.81738 15.2784 2.81738 15.1951 2.81738L14.1953 23.3131L21.3605 21.7301C21.3605 21.7301 18.861 4.73365 18.861 4.56701C18.861 4.4837 18.6944 4.40038 18.6111 4.40038Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M29.7751 13.3151C29.0253 12.8985 28.692 12.5653 28.692 12.1487C28.692 11.5655 29.1919 11.2322 30.0251 11.2322C30.9416 11.2322 31.7747 11.6488 31.7747 11.6488L32.4412 9.64922C32.4412 9.64922 31.858 9.14932 30.1084 9.14932C27.6089 9.14932 25.9426 10.5657 25.9426 12.5653C25.9426 13.7317 26.7758 14.5648 27.7756 15.1481C28.6087 15.648 28.942 15.9812 28.942 16.4811C28.942 16.981 28.5254 17.4809 27.6922 17.4809C26.5258 17.4809 25.3594 16.8977 25.3594 16.8977L24.6929 18.8973C24.6929 18.8973 25.6927 19.5638 27.4423 19.5638C29.9418 19.5638 31.7747 18.3141 31.7747 16.0645C31.7747 14.8148 30.8582 13.8983 29.7751 13.3151ZM39.8563 9.06601C38.6066 9.06601 37.6068 9.64922 36.857 10.5657L37.9401 4.90022H35.1073L32.3579 19.3138H35.1907L36.1071 14.3982C36.4404 12.5653 37.4402 11.3988 38.3567 11.3988C39.0232 11.3988 39.2731 11.8154 39.2731 12.482C39.2731 12.8985 39.2731 13.3151 39.1898 13.7317L38.1067 19.3972H40.9395L42.0226 13.5651C42.1059 12.9818 42.1892 12.232 42.1892 11.7321C42.1892 10.0658 41.356 9.06601 39.8563 9.06601ZM47.1048 17.3143C46.105 17.3143 45.7718 16.4811 45.7718 15.4813C45.7718 13.8983 46.6049 11.2322 48.1046 11.2322C49.1044 11.2322 49.4377 12.0654 49.4377 12.8985C49.4377 14.7315 48.6045 17.3143 47.1048 17.3143ZM48.5212 9.06601C45.1052 9.06601 42.8557 12.1487 42.8557 15.5646C42.8557 17.7308 44.1888 19.4805 46.7716 19.4805C50.1042 19.4805 52.3537 16.4811 52.3537 12.9818C52.3537 10.9823 51.1873 9.06601 48.5212 9.06601ZM56.8528 17.3976C56.1029 17.3976 55.6863 16.981 55.6863 16.981L56.1862 14.3149C56.5195 12.5653 57.436 11.3988 58.4358 11.3988C59.2689 11.3988 59.6022 12.232 59.6022 12.9818C59.5189 14.8148 58.4358 17.3976 56.8528 17.3976ZM59.6022 9.06601C57.6859 9.06601 56.6028 10.7323 56.6028 10.7323L56.7695 9.23264H54.27C54.1867 10.2324 53.9367 11.8154 53.6868 12.9818L51.6872 23.313H54.5199L55.2698 19.1472H55.3531C55.3531 19.1472 55.9363 19.4805 57.0194 19.4805C60.352 19.4805 62.5182 16.0645 62.5182 12.6486C62.5182 10.8156 61.6851 9.06601 59.6022 9.06601ZM66.5174 5.06685C65.6009 5.06685 64.9344 5.81669 64.9344 6.73316C64.9344 7.56632 65.4343 8.14953 66.2675 8.14953C67.1839 8.14953 67.9338 7.56632 67.9338 6.48322C67.9338 5.65006 67.3506 5.06685 66.5174 5.06685ZM62.6016 19.3138H65.4343L67.3506 9.31595H64.5178L62.6016 19.3138ZM74.5157 9.31595H72.5161L72.5995 8.81606C72.7661 7.81627 73.3493 6.98311 74.2658 6.98311C74.7657 6.98311 75.1822 7.14974 75.1822 7.14974L75.7655 4.90022C75.7655 4.90022 75.2656 4.65027 74.2658 4.65027C73.266 4.65027 72.2662 4.90022 71.5163 5.56674C70.5166 6.3999 70.1 7.56632 69.85 8.73274L69.7667 9.23264H68.4337L68.0171 11.3988H69.3501L67.8505 19.3138H70.6832L72.1829 11.3988H74.0991L74.5157 9.31595ZM81.2643 9.31595C81.2643 9.31595 79.5147 13.7317 78.6815 16.1478C78.5982 15.398 78.015 9.31595 78.015 9.31595H75.0156L76.6819 18.4807C76.6819 18.6473 76.6819 18.814 76.5986 18.9806C76.2654 19.6471 75.6821 20.2303 75.0989 20.6469C74.599 21.0635 73.9325 21.3134 73.5159 21.4801L74.2658 23.8962C74.849 23.8129 76.0154 23.313 77.0152 22.3965C78.2649 21.2301 79.5147 19.3138 80.6811 16.8144L84.097 9.39927L81.2643 9.31595Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9632_116870">
|
||||
<rect
|
||||
width="83.3158"
|
||||
height="23.8283"
|
||||
fill="currentColor"
|
||||
transform="translate(0.949219 0.585938)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export function VerizonLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="82"
|
||||
height="19"
|
||||
viewBox="0 0 82 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_9632_116867)">
|
||||
<path
|
||||
d="M79.373 0.50708L74.9504 9.96817L73.2874 6.3833H71.4888L74.2236 12.2472H75.6773L81.1593 0.50708H79.373Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M60.4008 18.0743H63.961V11.5575C63.961 10.0669 64.8234 9.01976 66.1046 9.01976C67.3365 9.01976 67.9771 9.8821 67.9771 11.1263V18.0743H71.5373V10.4488C71.5373 7.86176 69.9851 6.03853 67.472 6.03853C65.8705 6.03853 64.7495 6.7284 63.9118 7.98495H63.8379V6.38347H60.3885L60.4008 18.0743ZM53.4036 6.06317C49.6832 6.06317 47.1947 8.73642 47.1947 12.2597C47.1947 15.7583 49.6832 18.4562 53.4036 18.4562C57.1239 18.4562 59.6124 15.7583 59.6124 12.2597C59.6247 8.73642 57.1239 6.06317 53.4036 6.06317ZM53.3789 15.8199C51.6912 15.8199 50.7919 14.4032 50.7919 12.2597C50.7919 10.0915 51.6789 8.69946 53.3789 8.69946C55.0666 8.69946 56.0029 10.0915 56.0029 12.2597C56.0152 14.4032 55.079 15.8199 53.3789 15.8199ZM37.0438 18.0743H47.0469V15.2656H41.3801V15.1916L46.8005 9.11831V6.37115H37.0438V9.19223H42.5258V9.26614L37.0438 15.3887V18.0743ZM32.4734 18.0743H36.0582V6.38347H32.4734V18.0743ZM24.158 18.0743H27.7305V12.7032C27.7305 10.264 29.1965 9.17991 31.4262 9.48789H31.5002V6.40811C31.3154 6.33419 31.1059 6.32187 30.7733 6.32187C29.3813 6.32187 28.445 6.96247 27.6443 8.32989H27.5704V6.38347H24.158V18.0743ZM17.6042 15.8322C16.0273 15.8322 15.0664 14.8097 14.8817 13.2329H23.3079C23.3326 10.7198 22.6673 8.71178 21.2753 7.45523C20.2898 6.54362 18.9962 6.03853 17.3455 6.03853C13.8222 6.03853 11.383 8.71178 11.383 12.2104C11.383 15.7337 13.7113 18.4193 17.5795 18.4193C19.0455 18.4193 20.2035 18.025 21.1644 17.3721C22.1869 16.6699 22.9261 15.6351 23.1232 14.6126H19.6984C19.3658 15.3764 18.6513 15.8322 17.6042 15.8322ZM17.4194 8.58859C18.676 8.58859 19.5383 9.52484 19.6861 10.8923H14.9063C15.1896 9.51253 15.9411 8.58859 17.4194 8.58859ZM4.26258 18.0743H8.00759L12.0729 6.38347H8.48804L6.209 13.8981H6.15972L3.88068 6.38347H0.197266L4.26258 18.0743ZM32.4734 2.2196H36.0582V5.39794H32.4734V2.2196Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9632_116867">
|
||||
<rect
|
||||
width="81.0845"
|
||||
height="17.9859"
|
||||
fill="white"
|
||||
transform="translate(0.074707 0.50708)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -17,13 +17,13 @@ export function ActiveBadge({ active, className, badgeSize = "normal" }: ActiveB
|
||||
switch (active) {
|
||||
case true:
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-slate-800 text-green-500", className)}>
|
||||
<span className={cn(variant[badgeSize], "bg-charcoal-800 text-green-500", className)}>
|
||||
Active
|
||||
</span>
|
||||
);
|
||||
case false:
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-slate-800 text-dimmed", className)}>
|
||||
<span className={cn(variant[badgeSize], "bg-charcoal-800 text-text-dimmed", className)}>
|
||||
Disabled
|
||||
</span>
|
||||
);
|
||||
@@ -38,7 +38,7 @@ export function MissingIntegrationBadge({
|
||||
badgeSize?: keyof typeof variant;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-rose-600 text-white", className)}>
|
||||
<span className={cn(variant[badgeSize], "bg-error text-white", className)}>
|
||||
Missing Integration
|
||||
</span>
|
||||
);
|
||||
@@ -52,6 +52,8 @@ export function NewBadge({
|
||||
badgeSize?: keyof typeof variant;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn(variant[badgeSize], "bg-green-600 text-background", className)}>New!</span>
|
||||
<span className={cn(variant[badgeSize], "bg-green-600 text-background-dimmed", className)}>
|
||||
New!
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Header2 } from "./primitives/Headers";
|
||||
|
||||
export function BlankstateInstructions({
|
||||
children,
|
||||
className,
|
||||
title,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
{title && (
|
||||
<div className="flex h-10 items-center border-b border-grid-bright">
|
||||
<Header2>{title}</Header2>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,10 +13,10 @@ type ComingSoonProps = {
|
||||
export function ComingSoon({ title, description, icon }: ComingSoonProps) {
|
||||
return (
|
||||
<MainCenteredContainer>
|
||||
<div className="flex flex-col justify-center rounded border border-slate-800 bg-slate-850">
|
||||
<div className="flex items-center gap-2 border-b border-slate-750 px-4 py-4">
|
||||
<div className="flex flex-col justify-center rounded border border-charcoal-800 bg-charcoal-850">
|
||||
<div className="flex items-center gap-2 border-b border-charcoal-750 px-4 py-4">
|
||||
{typeof icon === "string" ? (
|
||||
<NamedIconInBox name={icon} className={"h-10 w-10 bg-midnight-800"} />
|
||||
<NamedIconInBox name={icon} className={"bg-midnight-800 h-10 w-10"} />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Header3 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./primitives/Tooltip";
|
||||
|
||||
export function DefinitionTip({
|
||||
content,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
content: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip disableHoverableContent>
|
||||
<TooltipTrigger>
|
||||
<span className="underline decoration-charcoal-600 decoration-dashed underline-offset-4 transition hover:decoration-charcoal-500">
|
||||
{children}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="end" side="right" variant="dark" className="w-[16rem] min-w-[16rem]">
|
||||
<Header3 className="mb-1">{title}</Header3>
|
||||
{typeof content === "string" ? (
|
||||
<Paragraph variant="small">{content}</Paragraph>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ type DisplayOptionsProps = {
|
||||
export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
|
||||
<Header1 className="mb-4 border-b border-charcoal-800 pb-4">{title}</Header1>
|
||||
{message && <Header3>{message}</Header3>}
|
||||
<LinkButton to={button ? button.to : "/"} variant="primary/medium" className="mt-8">
|
||||
{button ? button.title : "Home"}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { DiscordIcon, GitHubLightIcon } from "@trigger.dev/companyicons";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { Button, LinkButton } from "./primitives/Buttons";
|
||||
import { Fieldset } from "./primitives/Fieldset";
|
||||
import { FormButtons } from "./primitives/FormButtons";
|
||||
import { FormError } from "./primitives/FormError";
|
||||
import { Header1, Header2 } from "./primitives/Headers";
|
||||
import { InputGroup } from "./primitives/InputGroup";
|
||||
import { Label } from "./primitives/Label";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
@@ -18,10 +21,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./primitives/Select";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "./primitives/Sheet";
|
||||
import { Sheet, SheetBody, SheetContent, SheetTrigger } from "./primitives/Sheet";
|
||||
import { TextArea } from "./primitives/TextArea";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { ActivityIcon, HeartPulseIcon } from "lucide-react";
|
||||
import { docsPath } from "~/utils/pathBuilder";
|
||||
|
||||
type FeedbackProps = {
|
||||
button: ReactNode;
|
||||
@@ -55,19 +60,24 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild={true}>{button}</SheetTrigger>
|
||||
<SheetContent size="sm">
|
||||
<SheetHeader className="justify-between">Help & feedback</SheetHeader>
|
||||
<SheetBody>
|
||||
<DiscordBanner />
|
||||
<Paragraph variant="small" className="mb-4 border-t border-slate-800 pt-3">
|
||||
Or use this form to ask for help or give us feedback. We read every message and will get
|
||||
back to you as soon as we can.
|
||||
<SheetContent className="@container">
|
||||
<SheetBody className="flex h-full flex-col justify-between">
|
||||
<Header2 className="mb-2.5 text-xl">Get help from the community</Header2>
|
||||
<Paragraph className="mb-4">
|
||||
The quickest way to get help and feedback or to provide advice to others is to join our
|
||||
Discord.
|
||||
</Paragraph>
|
||||
<div className="flex flex-col gap-x-4 @[30rem]:flex-row">
|
||||
<DiscordBanner />
|
||||
<GitHubDiscussionsBanner />
|
||||
</div>
|
||||
<hr className="mb-4" />
|
||||
<Header2 className="mb-2.5 text-xl">Send us an email</Header2>
|
||||
<Paragraph className="mb-4">We read every message and respond quickly.</Paragraph>
|
||||
<Form method="post" action="/resources/feedback" {...form.props}>
|
||||
<Fieldset className="max-w-full">
|
||||
<Fieldset className="max-w-full gap-y-3">
|
||||
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
|
||||
<InputGroup className="max-w-full">
|
||||
<Label>How can we help?</Label>
|
||||
<SelectGroup>
|
||||
<Select {...conform.input(feedbackType)} defaultValue={defaultValue}>
|
||||
<SelectTrigger size="medium" width="full">
|
||||
@@ -90,43 +100,92 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
|
||||
<FormError id={message.errorId}>{message.error}</FormError>
|
||||
</InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
<FormButtons
|
||||
className="w-full"
|
||||
confirmButton={
|
||||
<Button type="submit" variant="primary/medium">
|
||||
Send
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex w-full justify-end">
|
||||
<FormButtons
|
||||
className="m-0 w-max"
|
||||
confirmButton={
|
||||
<Button type="submit" variant="tertiary/medium">
|
||||
Send message
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
<hr className="my-4" />
|
||||
<Header2 className="mb-2.5 text-xl">Troubleshooting</Header2>
|
||||
<Paragraph className="mb-4">
|
||||
If you're having trouble, check out our documentation or the Trigger.dev Status page.
|
||||
</Paragraph>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LinkButton to={docsPath("")} variant="tertiary/medium" LeadingIcon={BookOpenIcon}>
|
||||
Docs
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
to={docsPath("v3/introduction")}
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
v3 Docs (Developer preview)
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
to={"https://trigger.openstatus.dev/"}
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={ActivityIcon}
|
||||
>
|
||||
Trigger.dev Status
|
||||
</LinkButton>
|
||||
</div>
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function DiscordBanner() {
|
||||
function DiscordBanner({ className }: { className?: string }) {
|
||||
return (
|
||||
<a
|
||||
href="https://discord.gg/nkqV9xBYWy"
|
||||
href="https://trigger.dev/discord"
|
||||
target="_blank"
|
||||
className="group mb-4 flex w-full items-center justify-between rounded-md border border-slate-600 bg-gradient-to-br from-blue-400/30 to-indigo-400/50 p-4 transition hover:border-indigo-400"
|
||||
className={cn(
|
||||
"group mb-4 flex w-full items-center justify-between rounded-md border border-charcoal-600 p-4 transition hover:border-text-link",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DiscordIcon className="h-8 w-8" />
|
||||
<h2 className="font-title text-2xl text-bright transition group-hover:text-white">
|
||||
Join the Trigger.dev
|
||||
<br />
|
||||
Discord community
|
||||
</h2>
|
||||
<Paragraph variant="small">
|
||||
<Header1 className="text-2xl font-semibold text-text-bright transition group-hover:text-white">
|
||||
Join our Discord community
|
||||
</Header1>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Get help or answer questions from the Trigger.dev community.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<ChevronRightIcon className="h-5 w-5 text-slate-400 transition group-hover:translate-x-1 group-hover:text-indigo-400" />
|
||||
</div>
|
||||
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover:text-text-link" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubDiscussionsBanner({ className }: { className?: string }) {
|
||||
return (
|
||||
<a
|
||||
href="https://github.com/triggerdotdev/trigger.dev/discussions"
|
||||
target="_blank"
|
||||
className={cn(
|
||||
"group mb-4 flex w-full items-center justify-between rounded-md border border-charcoal-600 p-4 transition hover:border-text-dimmed",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<GitHubLightIcon className="mb-1 h-7 w-7" />
|
||||
<Header1 className="text-2xl font-semibold text-text-bright transition group-hover:text-white">
|
||||
View our GitHub Discussions
|
||||
</Header1>
|
||||
<Paragraph variant="small">
|
||||
Post your questions, feedback, and feature requests on GitHub.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover:text-text-bright" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,19 +16,23 @@ export type JobEnvironment = {
|
||||
lastRun?: Date;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
concurrencyLimit?: number | null;
|
||||
concurrencyLimitGroup?: { name: string; concurrencyLimit: number } | null;
|
||||
};
|
||||
|
||||
type JobStatusTableProps = {
|
||||
environments: JobEnvironment[];
|
||||
displayStyle?: "short" | "long";
|
||||
};
|
||||
|
||||
export function JobStatusTable({ environments }: JobStatusTableProps) {
|
||||
export function JobStatusTable({ environments, displayStyle = "short" }: JobStatusTableProps) {
|
||||
return (
|
||||
<Table fullWidth>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Env</TableHeaderCell>
|
||||
<TableHeaderCell>Last Run</TableHeaderCell>
|
||||
{displayStyle === "long" && <TableHeaderCell>Concurrency</TableHeaderCell>}
|
||||
<TableHeaderCell alignment="right">Version</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Status</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -42,6 +46,23 @@ export function JobStatusTable({ environments }: JobStatusTableProps) {
|
||||
<TableCell>
|
||||
{environment.lastRun ? <DateTime date={environment.lastRun} /> : "Never Run"}
|
||||
</TableCell>
|
||||
{displayStyle === "long" && (
|
||||
<TableCell>
|
||||
{environment.concurrencyLimitGroup ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<span>{environment.concurrencyLimitGroup.name}</span>
|
||||
<span className="text-gray-400">
|
||||
({environment.concurrencyLimitGroup.concurrencyLimit})
|
||||
</span>
|
||||
</span>
|
||||
) : typeof environment.concurrencyLimit === "number" ? (
|
||||
<span className="text-gray-400">{environment.concurrencyLimit}</span>
|
||||
) : (
|
||||
<span className="text-gray-400">Not specified</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell alignment="right">{environment.version}</TableCell>
|
||||
<TableCell alignment="right">
|
||||
<ActiveBadge active={environment.enabled} />
|
||||
|
||||
+33
-14
@@ -1,9 +1,16 @@
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Direction, RunList } from "~/presenters/RunListPresenter.server";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function ListPagination({ list, className }: { list: RunList; className?: string }) {
|
||||
type List = {
|
||||
pagination: {
|
||||
next?: string | undefined;
|
||||
previous?: string | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export function ListPagination({ list, className }: { list: List; className?: string }) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1", className)}>
|
||||
<PreviousButton cursor={list.pagination.previous} />
|
||||
@@ -15,31 +22,43 @@ export function ListPagination({ list, className }: { list: RunList; className?:
|
||||
function NextButton({ cursor }: { cursor?: string }) {
|
||||
const path = useCursorPath(cursor, "forward");
|
||||
|
||||
return path ? (
|
||||
return (
|
||||
<LinkButton
|
||||
to={path}
|
||||
variant={"tertiary/small"}
|
||||
TrailingIcon="chevron-right"
|
||||
className="flex items-center"
|
||||
to={path ?? "#"}
|
||||
variant={"minimal/small"}
|
||||
TrailingIcon="arrow-right"
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path &&
|
||||
"cursor-not-allowed opacity-50 group-hover:bg-transparent group-hover:text-text-dimmed"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
>
|
||||
Next
|
||||
</LinkButton>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousButton({ cursor }: { cursor?: string }) {
|
||||
const path = useCursorPath(cursor, "backward");
|
||||
|
||||
return path ? (
|
||||
return (
|
||||
<LinkButton
|
||||
to={path}
|
||||
variant={"tertiary/small"}
|
||||
LeadingIcon="chevron-left"
|
||||
className="flex items-center"
|
||||
to={path ?? "#"}
|
||||
variant={"minimal/small"}
|
||||
LeadingIcon="arrow-left"
|
||||
leadingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path &&
|
||||
"cursor-not-allowed opacity-50 group-hover:bg-transparent group-hover:text-text-dimmed"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
>
|
||||
Prev
|
||||
</LinkButton>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
function useCursorPath(cursor: string | undefined, direction: Direction) {
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ATAndTLogo } from "~/assets/logos/ATAndTLogo";
|
||||
import { AppsmithLogo } from "~/assets/logos/AppsmithLogo";
|
||||
import { CalComLogo } from "~/assets/logos/CalComLogo";
|
||||
import { OktaLogo } from "~/assets/logos/OktaLogo";
|
||||
import { ShopifyLogo } from "~/assets/logos/ShopifyLogo";
|
||||
import { VerizonLogo } from "~/assets/logos/VerizonLogo";
|
||||
import { LogoType } from "./LogoType";
|
||||
import { LinkButton } from "./primitives/Buttons";
|
||||
import { Header3 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { TextLink } from "./primitives/TextLink";
|
||||
|
||||
interface QuoteType {
|
||||
quote: string;
|
||||
person: string;
|
||||
}
|
||||
|
||||
const quotes: QuoteType[] = [
|
||||
{
|
||||
quote: "Trigger.dev is redefining background jobs for modern developers.",
|
||||
person: "Paul Copplestone, Supabase",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"Trigger.dev is a great way to automate email campaigns with Resend, and we've heard nothing but good things from our mutual customers.",
|
||||
person: "Zeno Rocha, Resend",
|
||||
},
|
||||
{
|
||||
quote: "We love Trigger.dev and it’s had a big impact in dev iteration velocity already.",
|
||||
person: "André Neves, ZBD",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"We’ve been looking for a product like Trigger.dev for a really long time - automation that's simple and developer-focused.",
|
||||
person: "Han Wang, Mintlify",
|
||||
},
|
||||
];
|
||||
|
||||
export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
||||
const [randomQuote, setRandomQuote] = useState<QuoteType | null>(null);
|
||||
useEffect(() => {
|
||||
const randomIndex = Math.floor(Math.random() * quotes.length);
|
||||
setRandomQuote(quotes[randomIndex]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="grid h-full grid-cols-1 md:grid-cols-2">
|
||||
<div className="border-r border-grid-bright bg-background-bright">
|
||||
<div className="flex h-full flex-col items-center justify-between p-6">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<a href="https://trigger.dev">
|
||||
<LogoType className="w-36" />
|
||||
</a>
|
||||
<LinkButton to="https://trigger.dev/docs" variant={"tertiary/small"} LeadingIcon="docs">
|
||||
Documentation
|
||||
</LinkButton>
|
||||
</div>
|
||||
<div className="flex h-full max-w-sm items-center justify-center">{children}</div>
|
||||
<Paragraph variant="small" className="text-center">
|
||||
Having login issues? <TextLink href="https://@trigger.dev/contact">Email us</TextLink>{" "}
|
||||
or <TextLink href="https://trigger.dev/discord">ask us in Discord</TextLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden grid-rows-[1fr_auto] pb-6 md:grid">
|
||||
<div className="flex h-full flex-col items-center justify-center px-16">
|
||||
<Header3 className="relative text-center text-2xl font-normal leading-8 text-text-dimmed transition before:relative before:right-1 before:top-0 before:text-6xl before:text-charcoal-750 before:content-['❝'] lg-height:text-xl md-height:text-lg">
|
||||
{randomQuote?.quote}
|
||||
</Header3>
|
||||
<Paragraph className="mt-4 text-text-dimmed/60">{randomQuote?.person}</Paragraph>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-4 px-8">
|
||||
<Paragraph>Trusted by developers at</Paragraph>
|
||||
<div className="flex w-full flex-wrap items-center justify-center gap-x-6 gap-y-3 text-charcoal-500 xl:justify-between xl:gap-0">
|
||||
<VerizonLogo />
|
||||
<ShopifyLogo />
|
||||
<OktaLogo />
|
||||
<ATAndTLogo />
|
||||
<AppsmithLogo />
|
||||
<CalComLogo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
export function LogoType({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 751 130" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path
|
||||
d="M195.022 16.2676H135.445H137.799V32.5096H157.858V102.4H174.84V32.5096H195.022V16.2676Z"
|
||||
fill="url(#paint0_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M211.265 51.4587V40.8767H195.391V102.4H211.265V72.9917C211.265 60.0719 221.725 56.3805 229.97 57.3648V39.6463C222.218 39.6463 214.465 43.0916 211.265 51.4587Z"
|
||||
fill="url(#paint1_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M246.954 33.494C252.368 33.494 256.799 29.0644 256.799 23.7734C256.799 18.4824 252.368 13.9297 246.954 13.9297C241.662 13.9297 237.232 18.4824 237.232 23.7734C237.232 29.0644 241.662 33.494 246.954 33.494ZM239.078 102.4H254.953V40.8767H239.078V102.4Z"
|
||||
fill="url(#paint2_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M315.253 40.8768V48.5056C310.946 42.7224 304.301 39.1542 295.563 39.1542C278.089 39.1542 264.921 53.4275 264.921 70.6539C264.921 88.0033 278.089 102.154 295.563 102.154C304.301 102.154 310.946 98.5853 315.253 92.8021V99.4466C315.253 109.167 309.1 114.581 299.132 114.581C289.656 114.581 285.596 110.767 283.011 105.968L269.475 113.72C274.889 123.687 285.472 128.731 298.64 128.731C314.884 128.731 330.758 119.626 330.758 99.4466V40.8768H315.253ZM298.025 87.5112C288.057 87.5112 280.796 80.4975 280.796 70.6539C280.796 60.9332 288.057 53.9196 298.025 53.9196C307.992 53.9196 315.253 60.9332 315.253 70.6539C315.253 80.4975 307.992 87.5112 298.025 87.5112Z"
|
||||
fill="url(#paint3_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M390.936 40.8768V48.5056C386.629 42.7224 379.983 39.1542 371.246 39.1542C353.772 39.1542 340.604 53.4275 340.604 70.6539C340.604 88.0033 353.772 102.154 371.246 102.154C379.983 102.154 386.629 98.5853 390.936 92.8021V99.4466C390.936 109.167 384.783 114.581 374.815 114.581C365.339 114.581 361.278 110.767 358.694 105.968L345.157 113.72C350.572 123.687 361.155 128.731 374.322 128.731C390.566 128.731 406.441 119.626 406.441 99.4466V40.8768H390.936ZM373.707 87.5112C363.739 87.5112 356.479 80.4975 356.479 70.6539C356.479 60.9332 363.739 53.9196 373.707 53.9196C383.675 53.9196 390.936 60.9332 390.936 70.6539C390.936 80.4975 383.675 87.5112 373.707 87.5112Z"
|
||||
fill="url(#paint4_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M432.9 78.1597H479.293C479.663 76.0679 479.909 73.9761 479.909 71.6383C479.909 53.5505 466.987 39.1542 448.775 39.1542C429.454 39.1542 416.287 53.3044 416.287 71.6383C416.287 89.9721 429.331 104.122 450.005 104.122C461.819 104.122 471.048 99.3236 476.832 90.9564L464.034 83.5737C461.327 87.142 456.404 89.726 450.251 89.726C441.883 89.726 435.115 86.2807 432.9 78.1597ZM432.654 65.8551C434.5 57.9802 440.284 53.4274 448.775 53.4274C455.42 53.4274 462.065 56.9958 464.034 65.8551H432.654Z"
|
||||
fill="url(#paint5_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M505.199 51.4587V40.8767H489.324V102.4H505.199V72.9917C505.199 60.0719 515.659 56.3805 523.904 57.3648V39.6463C516.151 39.6463 508.398 43.0916 505.199 51.4587Z"
|
||||
fill="url(#paint6_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M529.934 103.999C535.717 103.999 540.394 99.3235 540.394 93.5404C540.394 87.7572 535.717 83.0815 529.934 83.0815C524.15 83.0815 519.473 87.7572 519.473 93.5404C519.473 99.3235 524.15 103.999 529.934 103.999Z"
|
||||
fill="url(#paint7_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M596.632 16.2676V48.1364C592.202 42.4763 585.679 39.1541 576.696 39.1541C560.206 39.1541 546.67 53.3044 546.67 71.6382C546.67 89.972 560.206 104.122 576.696 104.122C585.679 104.122 592.202 100.8 596.632 95.1399V102.4H612.506V16.2676L596.632 16.2676ZM579.65 88.9876C569.805 88.9876 562.544 81.9741 562.544 71.6382C562.544 61.3024 569.805 54.2887 579.65 54.2887C589.371 54.2887 596.632 61.3024 596.632 71.6382C596.632 81.9741 589.371 88.9876 579.65 88.9876Z"
|
||||
fill="url(#paint8_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M637.98 78.1597H684.373C684.742 76.0679 684.989 73.9761 684.989 71.6383C684.989 53.5505 672.067 39.1542 653.855 39.1542C634.534 39.1542 621.367 53.3044 621.367 71.6383C621.367 89.9721 634.411 104.122 655.085 104.122C666.899 104.122 676.128 99.3236 681.912 90.9564L669.114 83.5737C666.407 87.142 661.484 89.726 655.331 89.726C646.963 89.726 640.195 86.2807 637.98 78.1597ZM637.734 65.8551C639.58 57.9802 645.363 53.4274 653.855 53.4274C660.5 53.4274 667.145 56.9958 669.114 65.8551H637.734Z"
|
||||
fill="url(#paint9_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
d="M732.859 40.8768L717.846 83.9428L702.955 40.8768H685.481L708.862 102.4H726.952L750.333 40.8768H732.859Z"
|
||||
fill="url(#paint10_linear_228_1439)"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M35.664 42.3949L59.4114 1.26865L118.264 103.194H0.558823L24.3062 62.0665L41.1046 71.7643L34.157 83.7971H84.6657L59.4114 40.0612L52.4637 52.094L35.664 42.3949Z"
|
||||
fill="url(#paint11_linear_228_1439)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_228_1439"
|
||||
x1="329.674"
|
||||
y1="150.079"
|
||||
x2="329.674"
|
||||
y2="13.9297"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_228_1439"
|
||||
x1="329.674"
|
||||
y1="150.079"
|
||||
x2="329.674"
|
||||
y2="13.9297"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_228_1439"
|
||||
x1="329.674"
|
||||
y1="150.079"
|
||||
x2="329.674"
|
||||
y2="13.9297"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_228_1439"
|
||||
x1="329.674"
|
||||
y1="150.079"
|
||||
x2="329.674"
|
||||
y2="13.9297"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_228_1439"
|
||||
x1="329.674"
|
||||
y1="150.079"
|
||||
x2="329.674"
|
||||
y2="13.9297"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint5_linear_228_1439"
|
||||
x1="329.674"
|
||||
y1="150.079"
|
||||
x2="329.674"
|
||||
y2="13.9297"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint6_linear_228_1439"
|
||||
x1="329.674"
|
||||
y1="150.079"
|
||||
x2="329.674"
|
||||
y2="13.9297"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint7_linear_228_1439"
|
||||
x1="634.903"
|
||||
y1="139.717"
|
||||
x2="651.436"
|
||||
y2="25.9719"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2563EB" />
|
||||
<stop offset="1" stopColor="#A855F7" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint8_linear_228_1439"
|
||||
x1="634.903"
|
||||
y1="139.717"
|
||||
x2="651.436"
|
||||
y2="25.9719"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2563EB" />
|
||||
<stop offset="1" stopColor="#A855F7" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint9_linear_228_1439"
|
||||
x1="634.903"
|
||||
y1="139.717"
|
||||
x2="651.436"
|
||||
y2="25.9719"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2563EB" />
|
||||
<stop offset="1" stopColor="#A855F7" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint10_linear_228_1439"
|
||||
x1="634.903"
|
||||
y1="139.717"
|
||||
x2="651.436"
|
||||
y2="25.9719"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2563EB" />
|
||||
<stop offset="1" stopColor="#A855F7" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint11_linear_228_1439"
|
||||
x1="95.8593"
|
||||
y1="103.194"
|
||||
x2="94.7607"
|
||||
y2="31.2381"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#41FF54" />
|
||||
<stop offset="1" stopColor="#E7FF52" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -7,9 +7,9 @@ export function NoMobileOverlay() {
|
||||
<>
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center sm:hidden">
|
||||
<div className="relative h-full w-full bg-black opacity-90"></div>
|
||||
<div className="absolute mx-8 flex flex-col items-center gap-6 rounded-lg bg-slate-850 px-8 py-10 text-center">
|
||||
<div className="absolute mx-8 flex flex-col items-center gap-6 rounded-lg bg-charcoal-850 px-8 py-10 text-center">
|
||||
<XMarkIcon className="absolute top-14 h-8 w-8 text-rose-600" />
|
||||
<DevicePhoneMobileIcon className="h-16 w-16 text-slate-500" />
|
||||
<DevicePhoneMobileIcon className="h-16 w-16 text-charcoal-500" />
|
||||
<Paragraph>Trigger.dev is currently only available on desktop.</Paragraph>
|
||||
<LinkButton to="https://trigger.dev" variant="primary/medium">
|
||||
Back Home
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import gradientBackground from "~/assets/images/gradient-background.png";
|
||||
|
||||
export function PageGradient({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className="-mt-4 h-full w-full bg-cover bg-no-repeat pt-20"
|
||||
style={{ backgroundImage: `url("${gradientBackground}")` }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export function ProductHuntBanner() {
|
||||
<LinkButton
|
||||
to="https://www.producthunt.com/posts/trigger-dev"
|
||||
target="_blank"
|
||||
className="!text-white underline underline-offset-2 transition hover:decoration-slate-100 hover:decoration-2"
|
||||
className="!text-white underline underline-offset-2 transition hover:decoration-charcoal-100 hover:decoration-2"
|
||||
variant="tertiary/small"
|
||||
>
|
||||
Vote for us today only!
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { InlineCode } from "./code/InlineCode";
|
||||
import {
|
||||
ClientTabs,
|
||||
@@ -128,3 +129,80 @@ export function TriggerDevStep({ extra }: { extra?: string }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger.dev version 3 setup commands
|
||||
const v3PackageTag = "beta";
|
||||
|
||||
export function InitCommandV3() {
|
||||
const project = useProject();
|
||||
const projectRef = project.ref;
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@${v3PackageTag} init -p ${projectRef}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@${v3PackageTag} init -p ${projectRef}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@${v3PackageTag} init -p ${projectRef}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDevStepV3() {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@${v3PackageTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@${v3PackageTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@${v3PackageTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ export function UserAvatar({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<UserCircleIcon className={cn("aspect-square text-slate-400", className)} />
|
||||
<UserCircleIcon className={cn("aspect-square text-charcoal-400", className)} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Label,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
const tooltipStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
borderRadius: "0.25rem",
|
||||
border: "1px solid #1A2434",
|
||||
backgroundColor: "#0B1018",
|
||||
padding: "0.3rem 0.5rem",
|
||||
fontSize: "0.75rem",
|
||||
color: "#E2E8F0",
|
||||
};
|
||||
|
||||
type DataItem = { date: string; maxConcurrentRuns: number };
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
export function ConcurrentRunsChart({
|
||||
concurrentRunsLimit,
|
||||
data,
|
||||
hasConcurrencyData,
|
||||
}: {
|
||||
concurrentRunsLimit?: number;
|
||||
data: DataItem[];
|
||||
hasConcurrencyData: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative">
|
||||
{!hasConcurrencyData && (
|
||||
<Paragraph className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
No concurrent Runs to show
|
||||
</Paragraph>
|
||||
)}
|
||||
<ResponsiveContainer width="100%" height="100%" className="relative min-h-[20rem]">
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 20,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 10,
|
||||
}}
|
||||
className="-ml-8"
|
||||
>
|
||||
<XAxis
|
||||
stroke="#94A3B8"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
dataKey={(item: DataItem) => {
|
||||
if (!item.date) return "";
|
||||
const date = new Date(item.date);
|
||||
if (date.getDate() === 1) {
|
||||
return dateFormatter.format(date);
|
||||
}
|
||||
return `${date.getDate()}`;
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Label value="Last 30 days" offset={-8} position="insideBottom" fill="#94A3B8" />
|
||||
</XAxis>
|
||||
<YAxis
|
||||
stroke="#94A3B8"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: "rgba(255,255,255,0.05)" }}
|
||||
contentStyle={tooltipStyle}
|
||||
labelFormatter={(value, data) => {
|
||||
const dateString = data.at(0)?.payload.date;
|
||||
if (!dateString) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return dateFormatter.format(new Date(dateString));
|
||||
}}
|
||||
/>
|
||||
{concurrentRunsLimit && (
|
||||
<ReferenceLine
|
||||
y={concurrentRunsLimit}
|
||||
stroke="#F43F5E"
|
||||
strokeWidth={1}
|
||||
strokeDasharray={5}
|
||||
ifOverflow="extendDomain"
|
||||
className="text-xs"
|
||||
>
|
||||
<Label
|
||||
value="Concurrent Runs limit"
|
||||
offset={8}
|
||||
position="insideTopLeft"
|
||||
fill="#F43F5E"
|
||||
/>
|
||||
</ReferenceLine>
|
||||
)}
|
||||
<Line
|
||||
dataKey="maxConcurrentRuns"
|
||||
name="Concurrent runs"
|
||||
stroke="#16A34A"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Label, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
const tooltipStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
borderRadius: "0.25rem",
|
||||
border: "1px solid #1A2434",
|
||||
backgroundColor: "#0B1018",
|
||||
padding: "0.3rem 0.5rem",
|
||||
fontSize: "0.75rem",
|
||||
color: "#E2E8F0",
|
||||
};
|
||||
|
||||
type DataItem = { date: string; runs: number };
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
export function DailyRunsChart({
|
||||
data,
|
||||
hasDailyRunsData,
|
||||
}: {
|
||||
data: DataItem[];
|
||||
hasDailyRunsData: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative">
|
||||
{!hasDailyRunsData && (
|
||||
<Paragraph className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
No daily Runs to show
|
||||
</Paragraph>
|
||||
)}
|
||||
<ResponsiveContainer width="100%" height="100%" className="relative min-h-[20rem]">
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 20,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 10,
|
||||
}}
|
||||
className="-ml-8"
|
||||
>
|
||||
<XAxis
|
||||
stroke="#94A3B8"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
dataKey={(item: DataItem) => {
|
||||
if (!item.date) return "";
|
||||
const date = new Date(item.date);
|
||||
if (date.getDate() === 1) {
|
||||
return dateFormatter.format(date);
|
||||
}
|
||||
return `${date.getDate()}`;
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Label value="Last 30 days" offset={-8} position="insideBottom" fill="#94A3B8" />
|
||||
</XAxis>
|
||||
<YAxis
|
||||
stroke="#94A3B8"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: "rgba(255,255,255,0.05)" }}
|
||||
contentStyle={tooltipStyle}
|
||||
labelFormatter={(value, data) => {
|
||||
const dateString = data.at(0)?.payload.date;
|
||||
if (!dateString) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return dateFormatter.format(new Date(dateString));
|
||||
}}
|
||||
/>
|
||||
<Line dataKey="runs" name="Runs" stroke="#16A34A" strokeWidth={2} dot={false} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { motion, useMotionValue, useTransform } from "framer-motion";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function FreePlanUsage({ to, percentage }: { to: string; percentage: number }) {
|
||||
const cappedPercentage = Math.min(percentage, 1);
|
||||
const widthProgress = useMotionValue(cappedPercentage * 100);
|
||||
const color = useTransform(
|
||||
widthProgress,
|
||||
[0, 74, 75, 95, 100],
|
||||
["#22C55E", "#22C55E", "#F59E0B", "#F43F5E", "#F43F5E"]
|
||||
);
|
||||
|
||||
const hasHitLimit = cappedPercentage >= 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded border border-charcoal-700 bg-charcoal-750 p-2.5",
|
||||
hasHitLimit && "border-error/40"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<ArrowUpCircleIcon className="h-5 w-5 text-text-dimmed" />
|
||||
<Paragraph className="text-2sm text-text-bright">Free Plan</Paragraph>
|
||||
</div>
|
||||
<Link to={to} className="text-2sm text-text-link">
|
||||
Learn more
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative mt-3 h-1 rounded-full bg-background-dimmed">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: cappedPercentage * 100 + "%" }}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
transition={{ duration: 1, type: "spring" }}
|
||||
className={cn("absolute left-0 top-0 h-full rounded-full")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import * as Slider from "@radix-ui/react-slider";
|
||||
import { Plans, estimate } from "@trigger.dev/billing";
|
||||
import { useCallback, useState } from "react";
|
||||
import { DefinitionTip } from "../DefinitionTooltip";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { formatCurrency, formatNumberCompact } from "~/utils/numberFormatter";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function PricingCalculator({ plans }: { plans: Plans }) {
|
||||
const [selectedConcurrencyIndex, setSelectedConcurrencyIndex] = useState(0);
|
||||
const concurrentRunTiers = [
|
||||
{ code: "free", upto: plans.free.concurrentRuns?.freeAllowance! },
|
||||
...(plans.paid.concurrentRuns?.pricing?.tiers ?? []),
|
||||
];
|
||||
const [runs, setRuns] = useState(0);
|
||||
const runBrackets = [
|
||||
...(plans.paid.runs?.pricing?.brackets.map((b, index, arr) => ({
|
||||
unitCost: b.unitCost,
|
||||
from: index === 0 ? 0 : arr[index - 1].upto! + 1,
|
||||
upto: b.upto ?? arr[index - 1].upto! * 10,
|
||||
})) ?? []),
|
||||
];
|
||||
|
||||
const result = estimate({
|
||||
usage: { runs, concurrent_runs: concurrentRunTiers[selectedConcurrencyIndex].upto - 1 },
|
||||
plans: [plans.free, plans.paid],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<ConcurrentRunsSlider
|
||||
options={concurrentRunTiers}
|
||||
selectedIndex={selectedConcurrencyIndex}
|
||||
setSelectedIndex={setSelectedConcurrencyIndex}
|
||||
cost={result?.cost.concurrentRunCost ?? 0}
|
||||
/>
|
||||
<RunsSlider
|
||||
brackets={runBrackets}
|
||||
runs={runs}
|
||||
setRuns={setRuns}
|
||||
cost={result?.cost.runsCost ?? 0}
|
||||
/>
|
||||
<GrandTotal cost={result?.cost.total ?? 0} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConcurrentRunsSlider({
|
||||
options,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
cost,
|
||||
}: {
|
||||
options: {
|
||||
code: string;
|
||||
upto: number;
|
||||
}[];
|
||||
selectedIndex: number;
|
||||
setSelectedIndex: (index: number) => void;
|
||||
cost: number;
|
||||
}) {
|
||||
const selectedOption = options[selectedIndex];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header2>
|
||||
<DefinitionTip
|
||||
content="The number of runs that can be executed at the same time. Get in touch if you need more than 100."
|
||||
title="Concurrent runs"
|
||||
>
|
||||
Concurrent runs
|
||||
</DefinitionTip>
|
||||
</Header2>
|
||||
<Header2>Up to {selectedOption.upto}</Header2>
|
||||
</div>
|
||||
<Slider.Root
|
||||
className="relative mb-2 mt-4 flex h-5 w-full touch-none select-none items-center"
|
||||
value={[selectedIndex]}
|
||||
onValueChange={(value) => setSelectedIndex(value[0])}
|
||||
max={options.length - 1}
|
||||
step={1}
|
||||
>
|
||||
<Slider.Track className="relative h-[8px] grow rounded-full bg-grid-bright">
|
||||
<Slider.Range className="absolute h-full rounded-full bg-primary" />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb
|
||||
className="block h-5 w-5 rounded-full border-4 border-primary bg-charcoal-850 shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] transition hover:border-primary hover:bg-charcoal-800 focus:shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] focus:outline-none"
|
||||
aria-label="Concurrent runs slider"
|
||||
/>
|
||||
</Slider.Root>
|
||||
<div className="ml-1.5 flex w-[99.85%] items-center justify-between">
|
||||
{options.map((tier, i) => {
|
||||
return (
|
||||
<Paragraph variant="extra-small" key={i}>
|
||||
{tier.upto}
|
||||
</Paragraph>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full items-start">
|
||||
<span className="ml-6 text-text-dimmed">=</span>
|
||||
<Header2 className="min-w-[8ch] text-right text-text-dimmed">
|
||||
{formatCurrency(cost, true)}
|
||||
</Header2>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-6 border-grid-bright" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const runIncrements = 10_000;
|
||||
function RunsSlider({
|
||||
brackets,
|
||||
runs,
|
||||
setRuns,
|
||||
cost,
|
||||
}: {
|
||||
brackets: {
|
||||
from: number;
|
||||
upto: number;
|
||||
unitCost: number;
|
||||
}[];
|
||||
runs: number;
|
||||
setRuns: (value: number) => void;
|
||||
cost: number;
|
||||
}) {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
const updateRuns = useCallback((value: number) => {
|
||||
setValue(value);
|
||||
const r = calculateRuns(value / runIncrements, brackets);
|
||||
setRuns(r);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header2>
|
||||
<DefinitionTip content="A single execution of a Job." title="Runs">
|
||||
Runs
|
||||
</DefinitionTip>
|
||||
</Header2>
|
||||
<Header2>{formatNumberCompact(runs)}</Header2>
|
||||
</div>
|
||||
<Slider.Root
|
||||
className="relative mb-2 mt-4 flex h-5 w-full touch-none select-none items-center"
|
||||
value={[value]}
|
||||
onValueChange={(value) => updateRuns(value[0])}
|
||||
max={runIncrements}
|
||||
step={1}
|
||||
>
|
||||
<Slider.Track className="relative h-[8px] grow rounded-full bg-grid-bright">
|
||||
<Slider.Range className="absolute h-full rounded-full bg-primary" />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb
|
||||
className="block h-5 w-5 rounded-full border-4 border-primary bg-charcoal-850 shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] transition hover:border-primary hover:bg-charcoal-800 focus:shadow-[0_1px_3px_4px_rgb(0_0_0_/_0.2),_0_1px_2px_-1px_rgb(0_0_0_/_0.1)] focus:outline-none"
|
||||
aria-label="Concurrent runs slider"
|
||||
/>
|
||||
</Slider.Root>
|
||||
<div className="relative w-full">
|
||||
{brackets.map((bracket, i, arr) => {
|
||||
const percentagePerBracket = 1 / arr.length;
|
||||
return (
|
||||
<SliderMarker
|
||||
key={i}
|
||||
percentage={(i / (arr.length - 1)) * percentagePerBracket * (arr.length - 1)}
|
||||
alignment={i === 0 ? "left" : "center"}
|
||||
text={formatNumberCompact(bracket.from)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<SliderMarker
|
||||
percentage={1}
|
||||
alignment={"right"}
|
||||
text={formatNumberCompact(brackets[brackets.length - 1].upto)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full items-start">
|
||||
<span className="ml-6 text-text-dimmed">=</span>
|
||||
<Header2 className="min-w-[8ch] text-right text-text-dimmed">
|
||||
{formatCurrency(cost, true)}
|
||||
</Header2>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-6 border-grid-bright" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function calculateRuns(percentage: number, brackets: { from: number; upto: number }[]) {
|
||||
//first we find which bucket we're in
|
||||
const buckets = brackets.length;
|
||||
const bucket = Math.min(Math.floor(percentage * buckets), brackets.length - 1);
|
||||
const percentagePerBucket = 1 / buckets;
|
||||
|
||||
//relevant bracket
|
||||
let bracket = brackets[bucket];
|
||||
const from = bracket.from;
|
||||
const upto = bracket.upto;
|
||||
|
||||
//how far as we into the bracket
|
||||
const percentageIntoBracket = (percentage - bucket * percentagePerBucket) / percentagePerBucket;
|
||||
|
||||
//calculate the runs
|
||||
const runs = Math.floor(from + (upto - from) * percentageIntoBracket);
|
||||
return runs;
|
||||
}
|
||||
|
||||
function GrandTotal({ cost }: { cost: number }) {
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<Header2>Total monthly estimate</Header2>
|
||||
<Header2>{formatCurrency(cost, true)}</Header2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SliderMarker({
|
||||
percentage,
|
||||
alignment,
|
||||
text,
|
||||
}: {
|
||||
percentage: number;
|
||||
alignment: "left" | "center" | "right";
|
||||
text: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="absolute top-0 h-4"
|
||||
style={{
|
||||
left: `${percentage * 100}%`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute flex items-center",
|
||||
alignment === "left"
|
||||
? "left-0 justify-start"
|
||||
: alignment === "center"
|
||||
? "-translate-x-1/2 justify-center"
|
||||
: "justify-middle right-0"
|
||||
)}
|
||||
>
|
||||
<Paragraph variant="extra-small">{text}</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { ActiveSubscription, Plan, Plans, SetPlanBodySchema } from "@trigger.dev/billing";
|
||||
import { useState } from "react";
|
||||
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { formatNumberCompact } from "~/utils/numberFormatter";
|
||||
import { DefinitionTip } from "../DefinitionTooltip";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import SegmentedControl from "../primitives/SegmentedControl";
|
||||
import { RunsVolumeDiscountTable } from "./RunsVolumeDiscountTable";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
|
||||
const pricingDefinitions = {
|
||||
concurrentRuns: {
|
||||
title: "Concurrent runs",
|
||||
content: "The number of runs that can be executed at the same time.",
|
||||
},
|
||||
jobRuns: {
|
||||
title: "Job runs",
|
||||
content: "A single execution of a job.",
|
||||
},
|
||||
jobs: {
|
||||
title: "Jobs",
|
||||
content: "A durable function that can be executed on a schedule or in response to an event.",
|
||||
},
|
||||
tasks: {
|
||||
title: "Tasks",
|
||||
content: "The individual building blocks of a job run.",
|
||||
},
|
||||
events: {
|
||||
title: "Events",
|
||||
content: "Events trigger jobs to start running.",
|
||||
},
|
||||
integrations: {
|
||||
title: "Integrations",
|
||||
content: "Easily subscribe to webhooks and perform actions using APIs.",
|
||||
},
|
||||
};
|
||||
|
||||
export function PricingTiers({
|
||||
organizationSlug,
|
||||
plans,
|
||||
className,
|
||||
showActionText = true,
|
||||
freeButtonPath,
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
plans: Plans;
|
||||
className?: string;
|
||||
showActionText?: boolean;
|
||||
freeButtonPath?: string;
|
||||
}) {
|
||||
const currentPlan = useCurrentPlan();
|
||||
//if they've canceled, we set the subscription to undefined so they can re-upgrade
|
||||
let currentSubscription = currentPlan?.subscription;
|
||||
if (currentPlan?.subscription?.canceledAt) {
|
||||
currentSubscription = undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-full flex-col items-start justify-center gap-4 md:flex-row",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<TierFree
|
||||
plan={plans.free}
|
||||
currentSubscription={currentSubscription}
|
||||
organizationSlug={organizationSlug}
|
||||
showActionText={showActionText}
|
||||
buttonPath={freeButtonPath}
|
||||
/>
|
||||
<TierPro
|
||||
plan={plans.paid}
|
||||
currentSubscription={currentSubscription}
|
||||
organizationSlug={organizationSlug}
|
||||
showActionText={showActionText}
|
||||
/>
|
||||
<TierEnterprise />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TierFree({
|
||||
plan,
|
||||
organizationSlug,
|
||||
showActionText,
|
||||
currentSubscription,
|
||||
buttonPath,
|
||||
}: {
|
||||
plan: Plan;
|
||||
organizationSlug: string;
|
||||
showActionText: boolean;
|
||||
currentSubscription?: ActiveSubscription;
|
||||
buttonPath?: string;
|
||||
}) {
|
||||
const lastSubmission = useActionData();
|
||||
const [form] = useForm({
|
||||
id: "subscribe",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: SetPlanBodySchema });
|
||||
},
|
||||
});
|
||||
|
||||
const navigation = useNavigation();
|
||||
const isLoading =
|
||||
(navigation.state === "submitting" || navigation.state === "loading") &&
|
||||
navigation.formData?.get("type") === "free";
|
||||
|
||||
const isCurrentPlan =
|
||||
currentSubscription?.isPaying === undefined || currentSubscription?.isPaying === false;
|
||||
|
||||
let actionText = "Select plan";
|
||||
|
||||
if (showActionText) {
|
||||
if (isCurrentPlan) {
|
||||
actionText = "Current Plan";
|
||||
} else {
|
||||
actionText = "Downgrade";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TierContainer>
|
||||
<Form action={`/resources/${organizationSlug}/subscribe`} method="post" {...form.props}>
|
||||
<Header title={plan.title} cost={0} />
|
||||
<TierLimit>
|
||||
Up to {plan.concurrentRuns?.freeAllowance}{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.concurrentRuns.title}
|
||||
content={pricingDefinitions.concurrentRuns.content}
|
||||
>
|
||||
{pricingDefinitions.concurrentRuns.title}
|
||||
</DefinitionTip>
|
||||
</TierLimit>
|
||||
<input type="hidden" name="type" value="free" />
|
||||
<div className="py-6">
|
||||
{buttonPath ? (
|
||||
<LinkButton
|
||||
variant="tertiary/large"
|
||||
fullWidth
|
||||
className="text-md font-medium"
|
||||
to={buttonPath}
|
||||
>
|
||||
{actionText}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary/large"
|
||||
fullWidth
|
||||
className="text-md font-medium"
|
||||
disabled={isLoading || isCurrentPlan}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Spinner color="white" />
|
||||
Updating plan
|
||||
</div>
|
||||
) : (
|
||||
actionText
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<FeatureItem checked>
|
||||
Up to {plan.runs?.freeAllowance ? formatNumberCompact(plan.runs.freeAllowance) : ""}{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.jobRuns.title}
|
||||
content={pricingDefinitions.jobRuns.content}
|
||||
>
|
||||
{pricingDefinitions.jobRuns.title}
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.jobs.title}
|
||||
content={pricingDefinitions.jobs.content}
|
||||
>
|
||||
jobs
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.tasks.title}
|
||||
content={pricingDefinitions.tasks.content}
|
||||
>
|
||||
tasks
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.events.title}
|
||||
content={pricingDefinitions.events.content}
|
||||
>
|
||||
events
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>Unlimited team members</FeatureItem>
|
||||
<FeatureItem checked>24 hour log retention</FeatureItem>
|
||||
<FeatureItem checked>Community support</FeatureItem>
|
||||
<FeatureItem>Custom integrations</FeatureItem>
|
||||
<FeatureItem>Role-based access control</FeatureItem>
|
||||
<FeatureItem>SSO</FeatureItem>
|
||||
<FeatureItem>On-prem option</FeatureItem>
|
||||
</ul>
|
||||
</Form>
|
||||
</TierContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function TierPro({
|
||||
plan,
|
||||
organizationSlug,
|
||||
showActionText,
|
||||
currentSubscription,
|
||||
}: {
|
||||
plan: Plan;
|
||||
organizationSlug: string;
|
||||
showActionText: boolean;
|
||||
currentSubscription?: ActiveSubscription;
|
||||
}) {
|
||||
const lastSubmission = useActionData();
|
||||
const [form] = useForm({
|
||||
id: "subscribe",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: SetPlanBodySchema });
|
||||
},
|
||||
});
|
||||
|
||||
const navigation = useNavigation();
|
||||
const isLoading =
|
||||
(navigation.state === "submitting" || navigation.state === "loading") &&
|
||||
navigation.formData?.get("planCode") === plan.code;
|
||||
|
||||
const currentConcurrencyTier = currentSubscription?.plan.concurrentRuns.pricing?.code;
|
||||
const [concurrentBracketCode, setConcurrentBracketCode] = useState(
|
||||
currentConcurrencyTier ?? plan.concurrentRuns?.pricing?.tiers[0].code
|
||||
);
|
||||
|
||||
const concurrencyTiers = plan.concurrentRuns?.pricing?.tiers ?? [];
|
||||
const selectedTier = concurrencyTiers.find((c) => c.code === concurrentBracketCode);
|
||||
|
||||
const freeRunCount = plan.runs?.pricing?.brackets[0].upto ?? 0;
|
||||
const mostExpensiveRunCost = plan.runs?.pricing?.brackets[1]?.unitCost ?? 0;
|
||||
|
||||
const isCurrentPlan = currentConcurrencyTier === concurrentBracketCode;
|
||||
|
||||
let actionText = "Select plan";
|
||||
|
||||
if (showActionText) {
|
||||
if (isCurrentPlan) {
|
||||
actionText = "Current Plan";
|
||||
} else {
|
||||
const currentTierIndex = concurrencyTiers.findIndex((c) => c.code === currentConcurrencyTier);
|
||||
const selectedTierIndex = concurrencyTiers.findIndex((c) => c.code === concurrentBracketCode);
|
||||
actionText = currentTierIndex < selectedTierIndex ? "Upgrade" : "Downgrade";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TierContainer isHighlighted>
|
||||
<Form action={`/resources/${organizationSlug}/subscribe`} method="post" {...form.props}>
|
||||
<Header title={plan.title} isHighlighted cost={selectedTier?.tierCost} />
|
||||
|
||||
<div className="mb-2 mt-6 font-sans text-sm font-normal text-text-bright">
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.concurrentRuns.title}
|
||||
content={pricingDefinitions.concurrentRuns.content}
|
||||
>
|
||||
{pricingDefinitions.concurrentRuns.title}
|
||||
</DefinitionTip>
|
||||
</div>
|
||||
<input type="hidden" name="type" value="paid" />
|
||||
<input type="hidden" name="planCode" value={plan.code} />
|
||||
<SegmentedControl
|
||||
name="concurrentRunBracket"
|
||||
options={concurrencyTiers.map((c) => ({ label: `Up to ${c.upto}`, value: c.code }))}
|
||||
fullWidth
|
||||
value={concurrentBracketCode}
|
||||
onChange={(v) => setConcurrentBracketCode(v)}
|
||||
/>
|
||||
<div className="py-6">
|
||||
<Button
|
||||
variant="primary/large"
|
||||
fullWidth
|
||||
className="text-md font-medium"
|
||||
type="submit"
|
||||
disabled={isLoading || isCurrentPlan}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Spinner color="white" />
|
||||
Updating plan
|
||||
</div>
|
||||
) : (
|
||||
actionText
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<FeatureItem checked>
|
||||
Includes {freeRunCount ? formatNumberCompact(freeRunCount) : ""}{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.jobRuns.title}
|
||||
content={pricingDefinitions.jobRuns.content}
|
||||
>
|
||||
{pricingDefinitions.jobRuns.title}
|
||||
</DefinitionTip>
|
||||
, then{" "}
|
||||
<DefinitionTip
|
||||
title="Runs volume discount"
|
||||
content={
|
||||
<RunsVolumeDiscountTable hideHeader brackets={plan.runs?.pricing?.brackets ?? []} />
|
||||
}
|
||||
>
|
||||
{"<"} ${(mostExpensiveRunCost * 1000).toFixed(2)}/1K runs
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.jobs.title}
|
||||
content={pricingDefinitions.jobs.content}
|
||||
>
|
||||
jobs
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.tasks.title}
|
||||
content={pricingDefinitions.tasks.content}
|
||||
>
|
||||
tasks
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.events.title}
|
||||
content={pricingDefinitions.events.content}
|
||||
>
|
||||
events
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>Unlimited team members</FeatureItem>
|
||||
<FeatureItem checked>7 day log retention</FeatureItem>
|
||||
<FeatureItem checked>Dedicated Slack support</FeatureItem>
|
||||
<FeatureItem>Custom integrations</FeatureItem>
|
||||
<FeatureItem>Role-based access control</FeatureItem>
|
||||
<FeatureItem>SSO</FeatureItem>
|
||||
<FeatureItem>On-prem option</FeatureItem>
|
||||
</ul>
|
||||
</Form>
|
||||
</TierContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function TierEnterprise() {
|
||||
return (
|
||||
<TierContainer>
|
||||
<Header title="Enterprise" />
|
||||
<TierLimit>
|
||||
Flexible{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.concurrentRuns.title}
|
||||
content={pricingDefinitions.concurrentRuns.content}
|
||||
>
|
||||
{pricingDefinitions.concurrentRuns.title}
|
||||
</DefinitionTip>
|
||||
</TierLimit>
|
||||
<div className="py-6">
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="secondary/large" fullWidth className="text-md font-medium">
|
||||
Contact us
|
||||
</Button>
|
||||
}
|
||||
defaultValue="enterprise"
|
||||
/>
|
||||
</div>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<FeatureItem checked>
|
||||
Flexible{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.jobRuns.title}
|
||||
content={pricingDefinitions.jobRuns.content}
|
||||
>
|
||||
{pricingDefinitions.jobRuns.title}
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.jobs.title}
|
||||
content={pricingDefinitions.jobs.content}
|
||||
>
|
||||
jobs
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.tasks.title}
|
||||
content={pricingDefinitions.tasks.content}
|
||||
>
|
||||
tasks
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Unlimited{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.events.title}
|
||||
content={pricingDefinitions.events.content}
|
||||
>
|
||||
events
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>Unlimited team members</FeatureItem>
|
||||
<FeatureItem checked>30 day log retention</FeatureItem>
|
||||
<FeatureItem checked>Priority support</FeatureItem>
|
||||
<FeatureItem checked>
|
||||
Custom{" "}
|
||||
<DefinitionTip
|
||||
title={pricingDefinitions.integrations.title}
|
||||
content={pricingDefinitions.integrations.content}
|
||||
>
|
||||
{pricingDefinitions.integrations.title}
|
||||
</DefinitionTip>
|
||||
</FeatureItem>
|
||||
<FeatureItem checked>Role-based access control</FeatureItem>
|
||||
<FeatureItem checked>SSO</FeatureItem>
|
||||
<FeatureItem checked>On-prem option</FeatureItem>
|
||||
</ul>
|
||||
</TierContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function TierContainer({
|
||||
children,
|
||||
isHighlighted,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isHighlighted?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full min-w-[16rem] flex-col rounded-md border p-6",
|
||||
isHighlighted ? "border-primary" : "border-grid-bright"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Header({
|
||||
title,
|
||||
cost: flatCost,
|
||||
isHighlighted,
|
||||
}: {
|
||||
title: string;
|
||||
cost?: number;
|
||||
isHighlighted?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2
|
||||
className={cn("text-xl font-medium", isHighlighted ? "text-primary" : "text-text-dimmed")}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{flatCost === 0 || flatCost ? (
|
||||
<h3 className="text-4xl font-medium text-text-bright">
|
||||
${flatCost}
|
||||
<span className="text-2sm font-normal tracking-wide text-text-dimmed">/month</span>
|
||||
</h3>
|
||||
) : (
|
||||
<h2 className="text-4xl font-medium">Custom</h2>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TierLimit({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<hr className="my-[1.9rem]" />
|
||||
<div className="mb-[0.6rem] mt-6 font-sans text-sm font-normal text-text-bright">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureItem({ checked, children }: { checked?: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<li className="flex items-center gap-2">
|
||||
{checked ? (
|
||||
<CheckIcon className="h-4 w-4 text-primary" />
|
||||
) : (
|
||||
<XMarkIcon className="h-4 w-4 text-charcoal-500" />
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"font-sans text-sm font-normal",
|
||||
checked ? "text-text-bright" : "text-text-dimmed"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { RunPriceBracket } from "@trigger.dev/billing";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { formatNumberCompact } from "~/utils/numberFormatter";
|
||||
|
||||
export function RunsVolumeDiscountTable({
|
||||
className,
|
||||
hideHeader = false,
|
||||
brackets,
|
||||
}: {
|
||||
className?: string;
|
||||
hideHeader?: boolean;
|
||||
brackets: RunPriceBracket[];
|
||||
}) {
|
||||
const runsVolumeDiscountRow =
|
||||
"flex justify-between whitespace-nowrap border-b gap-16 border-grid-bright last:pb-0 last:border-none py-2";
|
||||
|
||||
const bracketData = bracketInfo(brackets);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{hideHeader ? null : <Header2 className="mb-2">Runs volume discount</Header2>}
|
||||
<ul>
|
||||
{bracketData.map((bracket, index) => (
|
||||
<li key={index} className={runsVolumeDiscountRow}>
|
||||
<Paragraph variant="small">{bracket.range}</Paragraph>
|
||||
<Paragraph variant="small">{bracket.costLabel}</Paragraph>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function bracketInfo(brackets: RunPriceBracket[]) {
|
||||
return brackets.map((bracket, index) => {
|
||||
const { upto, unitCost } = bracket;
|
||||
|
||||
if (index === 0) {
|
||||
return {
|
||||
range: `First ${formatNumberCompact(upto!)}/mo`,
|
||||
costLabel: "Free",
|
||||
};
|
||||
}
|
||||
|
||||
const from = brackets[index - 1].upto;
|
||||
const fromFormatted = formatNumberCompact(from!);
|
||||
const toFormatted = upto ? formatNumberCompact(upto) : undefined;
|
||||
|
||||
const costLabel = `$${(unitCost * 1000).toFixed(2)}/1,000`;
|
||||
|
||||
if (!upto) {
|
||||
return {
|
||||
range: `${fromFormatted} +`,
|
||||
costLabel,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
range: `${fromFormatted}–${toFormatted}`,
|
||||
costLabel,
|
||||
};
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user