Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75753ec5ad | |||
| 40e0d2f7e9 | |||
| 246eb4361e | |||
| ab6e6afbf5 | |||
| 7445d9bf35 | |||
| f6eeaa1001 | |||
| eaf46bac58 | |||
| 16cf357380 | |||
| f96bf7210b | |||
| c855e4a96c | |||
| e1497c802d | |||
| e2612224b4 | |||
| 0d38ea0feb | |||
| 0687ce722c | |||
| d627fa718e | |||
| 5798024a5b | |||
| 5750f35add | |||
| 11f2832ae5 | |||
| fd04ed50ef | |||
| 6d17443e16 | |||
| dd321e3616 | |||
| 2ad664d17d | |||
| 5eb30d026d | |||
| 719ae83057 | |||
| 7eda22851e | |||
| b3aa87ee26 | |||
| 14ce599218 | |||
| 0a18e1d919 | |||
| ca51f65394 | |||
| 46a37fdc49 | |||
| 24383dc65d | |||
| e13edd3a20 | |||
| 9e0f03623d | |||
| 914ceaf3c5 | |||
| 472e5f93e1 | |||
| 51a054f534 | |||
| efd61f1374 | |||
| 2c7f7b1d5e | |||
| c92edd61af | |||
| ab69b60348 | |||
| d52e3d7d57 | |||
| 3ca0c15161 | |||
| 0aa597104c | |||
| 34f8bd588e | |||
| 4243cb2451 | |||
| 7e8a0d96d2 | |||
| 4d19dec71a | |||
| 76a5ac293e | |||
| 668b34d25e | |||
| 6a2d033f32 | |||
| e71df640a9 | |||
| 330f6c548e | |||
| 4d2412a0fe | |||
| 21a4fabc28 | |||
| 3418aa614b | |||
| 88a0754ae0 | |||
| f3e4597ef4 | |||
| 2bf04ece54 | |||
| 295b2eb3b6 | |||
| f0182abc83 | |||
| 1105b9b71a | |||
| 7077dbf65e | |||
| 6b355ab9ad | |||
| 68d7139ebe | |||
| c58f6f7a68 | |||
| 6516e15bd8 | |||
| 7f2365f4e0 | |||
| f560594610 | |||
| ebd9767926 | |||
| 1c631a1b6f | |||
| 110a8ea48c | |||
| 10f0ef3301 | |||
| 2e4a6307b4 |
+25
-3
@@ -31,10 +31,32 @@ DEV_OTEL_BATCH_PROCESSING_ENABLED="0"
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
|
||||
# Resend is an email service used for signing in to Trigger.dev via a Magic Link.
|
||||
# Emails will print to the console if you leave these commented out
|
||||
# Configure an email transport to allow users to sign in to Trigger.dev via a Magic Link.
|
||||
# If none are configured, emails will print to the console instead.
|
||||
# Uncomment one of the following blocks to allow delivery of
|
||||
|
||||
# Resend
|
||||
### Visit https://resend.com, create an account and get your API key. Then insert it below along with your From and Reply To email addresses. Visit https://resend.com/docs for more information.
|
||||
# RESEND_API_KEY=<api_key>
|
||||
# EMAIL_TRANSPORT=resend
|
||||
# FROM_EMAIL=
|
||||
# REPLY_TO_EMAIL=
|
||||
# RESEND_API_KEY=
|
||||
|
||||
# Generic SMTP
|
||||
### Enter the configuration provided by your mail provider. Visit https://nodemailer.com/smtp/ for more information
|
||||
### SMTP_SECURE = false will use STARTTLS when connecting to a server that supports it (usually port 587)
|
||||
# EMAIL_TRANSPORT=smtp
|
||||
# FROM_EMAIL=
|
||||
# REPLY_TO_EMAIL=
|
||||
# SMTP_HOST=
|
||||
# SMTP_PORT=587
|
||||
# SMTP_SECURE=false
|
||||
# SMTP_USER=
|
||||
# SMTP_PASSWORD=
|
||||
|
||||
# AWS Simple Email Service
|
||||
### Authentication is configured using the default Node.JS credentials provider chain (https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromnodeproviderchain)
|
||||
# EMAIL_TRANSPORT=aws-ses
|
||||
# FROM_EMAIL=
|
||||
# REPLY_TO_EMAIL=
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [buildjet-8vcpu-ubuntu-2204, windows-latest]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
package-manager: ["npm", "pnpm"]
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
name: Preview Release
|
||||
needs: [typecheck, units, e2e]
|
||||
if: github.repository == 'triggerdotdev/trigger.dev'
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
package: [coordinator, docker-provider, kubernetes-provider]
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests"
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
Vendored
+9
@@ -22,6 +22,15 @@
|
||||
"cwd": "${workspaceFolder}/apps/webapp",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug fairDequeuingStrategy.test.ts",
|
||||
"command": "pnpm run test -t FairDequeuingStrategy",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"cwd": "${workspaceFolder}/apps/webapp",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export function KeyboardDownIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.8779 16.3905C12.4989 17.0854 11.5011 17.0854 11.1221 16.3905L6.80665 8.47885C6.44317 7.81248 6.92548 7 7.68454 7L16.3155 7C17.0745 7 17.5568 7.81248 17.1934 8.47885L12.8779 16.3905Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function KeyboardLeftIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.60947 12.8779C6.91461 12.4989 6.91461 11.5011 7.60947 11.1221L15.5211 6.80665C16.1875 6.44317 17 6.92548 17 7.68454L17 16.3155C17 17.0745 16.1875 17.5568 15.5211 17.1934L7.60947 12.8779Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function KeyboardRightIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16.3905 11.1221C17.0854 11.5011 17.0854 12.4989 16.3905 12.8779L8.47885 17.1934C7.81248 17.5568 7 17.0745 7 16.3155L7 7.68454C7 6.92548 7.81248 6.44317 8.47885 6.80665L16.3905 11.1221Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function KeyboardUpIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.1221 6.60947C11.5011 5.91461 12.4989 5.91461 12.8779 6.60947L17.1934 14.5211C17.5568 15.1875 17.0745 16 16.3155 16L7.68454 16C6.92548 16 6.44317 15.1875 6.80665 14.5211L11.1221 6.60947Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function KeyboardWindowsIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="12"
|
||||
height="16"
|
||||
viewBox="0 0 12 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="1.66666" y="3.66663" width="4" height="4" fill="currentColor" />
|
||||
<rect x="6.33334" y="3.66663" width="4" height="4" fill="currentColor" />
|
||||
<rect x="1.66666" y="8.33337" width="4" height="4" fill="currentColor" />
|
||||
<rect x="6.33334" y="8.33337" width="4" height="4" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export function LyftLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="48"
|
||||
height="34"
|
||||
viewBox="0 0 48 34"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 0.593341H7.28904V20.1709C7.28904 23.2692 8.70514 25.1147 9.82493 25.9058C8.63943 26.9605 5.01673 27.8834 2.3162 25.6421C0.724053 24.3209 0 22.1485 0 20.1049V0.593341ZM45.5536 16.856V14.7986H47.7767V7.58069H45.3354C44.3982 3.24724 40.5432 0 35.9328 0C30.6217 0 26.3164 4.30894 26.3164 9.62402V26.48C27.8295 26.6928 29.6322 26.4536 31.1659 25.1807C32.7578 23.8594 33.4818 21.6872 33.4818 19.6436V19.0226H37.1222V11.8047H33.4818V9.62402H33.4905C33.4905 8.27426 34.5839 7.18004 35.9328 7.18004C37.2815 7.18004 38.3792 8.27426 38.3792 9.62402V16.856C38.3792 22.1711 42.6891 26.48 48 26.48V19.3C46.6513 19.3 45.5536 18.2057 45.5536 16.856ZM17.9474 7.58069V18.1737C17.9474 18.7751 17.4488 19.2626 16.8337 19.2626C16.2185 19.2626 15.7199 18.7751 15.7199 18.1737V7.58069H8.50752V20.0392C8.50752 22.2803 9.26697 25.1147 12.7231 26.0376C16.1828 26.9615 18.1899 25.049 18.1899 25.049C18.007 26.3089 16.8213 27.2318 14.9113 27.4296C13.4661 27.5791 11.6178 27.1 10.6959 26.7045V33.305C13.0451 33.9983 15.5298 34.2223 17.9614 33.7501C22.3744 32.8932 25.1596 29.2019 25.1596 24.2908V7.58069H17.9474Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export function MiddayLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={102}
|
||||
height={30}
|
||||
viewBox="0 0 100 30"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M14.347 0a14.931 14.931 0 0 0-6.282 1.68l6.282 10.88V0Zm0 17.443L8.067 28.32a14.933 14.933 0 0 0 6.28 1.68V17.443ZM15.652 30V17.432l6.285 10.887A14.932 14.932 0 0 1 15.652 30Zm0-17.43V0c2.26.097 4.392.693 6.287 1.682l-6.287 10.889ZM2.336 23.068l10.884-6.284-6.284 10.884a15.093 15.093 0 0 1-4.6-4.6Zm25.33-16.132-10.88 6.282 6.282-10.88a15.094 15.094 0 0 1 4.598 4.598ZM2.335 6.934a15.094 15.094 0 0 1 4.6-4.6l6.284 10.884L2.335 6.934Zm-.654 1.13A14.931 14.931 0 0 0 0 14.35h12.568L1.681 8.064Zm0 13.873a14.932 14.932 0 0 1-1.68-6.282h12.562L1.682 21.938Zm15.754-7.587H30a14.93 14.93 0 0 0-1.68-6.285L17.435 14.35Zm10.884 7.586-10.878-6.28H30a14.932 14.932 0 0 1-1.68 6.28Zm-11.533-5.151 6.281 10.88a15.092 15.092 0 0 0 4.598-4.599l-10.88-6.281Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M92.34 11.912h1.637l2.995 8.223 2.884-8.223h1.619l-4 11.107c-.372 1.06-1.08 1.544-2.196 1.544h-1.172v-1.358h1.024c.502 0 .8-.186.986-.707l.353-.912h-.52l-3.61-9.674ZM82.744 14.814c.39-1.916 1.916-3.126 4.018-3.126 2.549 0 3.963 1.489 3.963 4.13v3.964c0 .446.186.632.614.632h.39v1.358h-.65c-1.005 0-1.88-.335-1.861-1.544-.428.93-1.544 1.767-3.107 1.767-1.954 0-3.535-1.041-3.535-2.79 0-2.028 1.544-2.55 3.702-2.977l2.921-.558c-.018-1.712-.818-2.53-2.437-2.53-1.265 0-2.102.65-2.4 1.804l-1.618-.13Zm1.432 4.39c0 .8.689 1.452 2.14 1.433 1.637 0 2.92-1.153 2.92-3.442v-.167l-2.362.41c-1.47.26-2.698.371-2.698 1.767ZM80.129 8.563v13.21h-1.377l-.056-1.452c-.558 1.042-1.618 1.675-3.144 1.675-2.847 0-4.168-2.419-4.168-5.154s1.321-5.153 4.168-5.153c1.451 0 2.493.558 3.051 1.562V8.563h1.526Zm-7.145 8.28c0 1.915.819 3.701 2.884 3.701 2.028 0 2.865-1.823 2.865-3.702 0-1.953-.837-3.758-2.865-3.758-2.065 0-2.884 1.786-2.884 3.758ZM68.936 8.563v13.21H67.56l-.056-1.452c-.558 1.042-1.619 1.675-3.144 1.675-2.847 0-4.168-2.419-4.168-5.154s1.321-5.153 4.168-5.153c1.45 0 2.493.558 3.05 1.562V8.563h1.526Zm-7.144 8.28c0 1.915.819 3.701 2.884 3.701 2.028 0 2.865-1.823 2.865-3.702 0-1.953-.837-3.758-2.865-3.758-2.065 0-2.884 1.786-2.884 3.758ZM56.212 11.912h1.525v9.86h-1.525v-9.86Zm-.037-1.544V8.6h1.6v1.768h-1.6ZM40.224 11.912h1.395l.056 1.674c.446-1.21 1.47-1.898 2.846-1.898 1.414 0 2.438.763 2.865 2.084.428-1.34 1.47-2.084 3.014-2.084 1.973 0 3.126 1.377 3.126 3.74v6.344H52v-5.897c0-1.805-.707-2.828-1.916-2.828-1.544 0-2.437 1.041-2.437 2.846v5.88H46.12v-5.899c0-1.767-.725-2.827-1.916-2.827-1.526 0-2.456 1.079-2.456 2.827v5.898h-1.525v-9.86Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export function TldrawLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="96"
|
||||
height="25"
|
||||
viewBox="0 0 96 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.3031 0.283066C15.4501 0.283066 18.5971 0.281726 21.7441 0.283066C23.5701 0.283066 24.6473 1.36288 24.6486 3.19829C24.6513 9.4695 24.6513 15.7407 24.6486 22.0133C24.6486 23.8607 23.5808 24.9325 21.7521 24.9338C15.4581 24.9352 9.16417 24.9352 2.87018 24.9338C1.08836 24.9338 0.000510729 23.8446 -0.000828987 22.0521C-0.0021687 15.7581 -0.0021687 9.46414 0.000510729 3.17015C0.00185044 1.34546 1.08568 0.284406 2.93047 0.283066C6.05469 0.281726 9.18024 0.283066 12.3045 0.283066H12.3031ZM12.1223 16.9518C12.0499 17.7543 11.6386 18.3116 11.088 18.8006C10.8201 19.0391 10.4798 19.3713 10.8308 19.6902C11.0532 19.8938 11.5475 19.9997 11.8356 19.9019C13.7808 19.2374 15.2183 16.2498 14.582 14.3019C14.2725 13.3533 13.2825 12.6178 12.3674 12.6594C11.4095 12.7022 10.5682 13.4016 10.3525 14.3327C10.0819 15.4982 10.6553 16.3637 12.1236 16.9518H12.1223ZM12.4143 5.20384C11.2354 5.21858 10.315 6.1604 10.3257 7.34337C10.3364 8.52768 11.2662 9.47218 12.4331 9.48155C13.6134 9.49093 14.586 8.49016 14.5605 7.29112C14.5351 6.10815 13.5892 5.18776 12.4157 5.2025L12.4143 5.20384Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M85.3405 16.6586C85.4195 16.4443 85.5267 16.2353 85.5722 16.0142C85.9849 13.9846 86.3961 11.9549 86.786 9.92119C86.8784 9.44024 87.1035 9.22588 87.6006 9.25402C88.3093 9.29287 89.2229 9.03832 89.6731 9.39067C90.1206 9.73899 90.138 10.6768 90.284 11.3681C90.6176 12.9597 90.9096 14.5606 91.2339 16.1536C91.2674 16.317 91.408 16.459 91.4978 16.6117C91.5808 16.455 91.7269 16.3023 91.7389 16.1415C91.889 14.1185 92.0323 12.0942 92.1449 10.0699C92.1784 9.48043 92.4021 9.23124 93.0063 9.25134C93.9602 9.28215 94.9167 9.25937 95.9992 9.25937C95.8612 10.157 95.7433 10.9809 95.604 11.8022C95.1793 14.3061 94.7292 16.8073 94.3259 19.3153C94.2201 19.9731 93.995 20.308 93.2582 20.2598C92.3472 20.1995 91.4268 20.2008 90.5158 20.2598C89.8781 20.3013 89.6316 20.0227 89.5284 19.4586C89.2564 17.9689 88.9724 16.4818 88.683 14.9947C88.6415 14.783 88.5397 14.5847 88.4647 14.3798L88.2543 14.3945C88.182 14.7026 88.1029 15.0094 88.0373 15.3189C87.7412 16.735 87.4425 18.1524 87.1598 19.5712C87.0593 20.0776 86.7873 20.2692 86.2702 20.2531C85.2668 20.2236 84.262 20.2249 83.2585 20.2531C82.807 20.2651 82.6048 20.0736 82.5351 19.6596C81.9979 16.4724 81.4526 13.2879 80.9247 10.0994C80.8149 9.4322 80.985 9.26607 81.6763 9.26071C82.4346 9.25536 83.1942 9.28081 83.9525 9.25134C84.3799 9.23526 84.538 9.37995 84.5688 9.81C84.7148 11.9013 84.8863 13.9899 85.0511 16.0785C85.0645 16.25 85.0872 16.4202 85.106 16.5916L85.3405 16.6586Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M58.7188 5.17457C58.7188 5.4961 58.7188 5.73457 58.7188 5.97438C58.7188 10.3928 58.7188 14.8125 58.7188 19.2309C58.7188 20.1981 58.6692 20.2437 57.6832 20.245C56.7266 20.245 55.7701 20.245 54.7492 20.245C54.7305 19.8846 54.7332 19.5778 54.6943 19.2764C54.6648 19.05 54.5844 18.8316 54.5268 18.6092C54.3446 18.7432 54.1276 18.8477 53.9856 19.0165C53.3988 19.7132 52.7678 20.3107 51.7952 20.4178C50.0951 20.6041 48.6656 19.7091 48.5263 18.0157C48.3534 15.9151 48.399 13.7863 48.5075 11.6775C48.5731 10.3968 49.3663 9.47371 50.6618 9.16424C52.0082 8.84405 53.1804 9.15888 54.0472 10.3244C54.1651 10.4825 54.3621 10.583 54.5215 10.7103C54.5818 10.512 54.6903 10.3137 54.693 10.1141C54.7077 8.75295 54.7198 7.39046 54.693 6.03065C54.6809 5.38892 54.9435 5.15045 55.5691 5.16653C56.5913 5.19332 57.6135 5.17323 58.7188 5.17323V5.17457ZM54.6956 14.6879C54.6956 14.6879 54.6983 14.6879 54.6997 14.6879C54.6997 14.2431 54.7064 13.7983 54.6983 13.3535C54.6863 12.6984 54.4009 12.4827 53.522 12.4465C52.816 12.4171 52.4181 12.681 52.3953 13.2839C52.3605 14.1949 52.3578 15.1086 52.394 16.0182C52.4235 16.7778 53.1175 17.086 54.149 16.7658C54.3728 16.6961 54.6313 16.3866 54.6715 16.1522C54.7532 15.6766 54.6956 15.1782 54.6956 14.6892V14.6879Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M79.2702 14.5379C79.2702 14.9612 79.2809 15.3859 79.2675 15.8093C79.2528 16.3103 79.2072 16.7886 79.9052 16.9105C80.0419 16.9346 80.1999 17.2937 80.2107 17.504C80.2482 18.2609 80.2442 19.0219 80.2107 19.7788C80.204 19.9383 79.991 20.2276 79.8891 20.2209C78.942 20.1566 77.9747 20.1312 77.061 19.9034C76.5974 19.7882 76.1821 19.3247 75.8365 18.9375C75.5176 18.5811 75.3328 18.5651 75.0796 18.9563C74.1592 20.3804 72.7833 20.6202 71.2587 20.3549C69.998 20.1352 69.1861 19.1559 69.1192 17.8805C69.1004 17.5241 69.1031 17.1664 69.1084 16.81C69.1299 14.988 70.1775 13.8948 72.0036 13.8265C72.873 13.7943 73.7439 13.8359 74.6133 13.805C74.8692 13.7957 75.1197 13.6537 75.3743 13.5733C75.2376 13.241 75.18 12.8257 74.9416 12.6006C74.7326 12.4037 74.3159 12.3314 74.0038 12.3582C72.8087 12.456 71.6124 12.5725 70.4321 12.7735C69.8815 12.8672 69.6778 12.7574 69.6872 12.2108C69.6993 11.5423 69.6483 10.8684 69.7127 10.2052C69.7354 9.96676 69.9712 9.60906 70.1789 9.54877C72.2661 8.94992 74.3896 8.7436 76.5171 9.29556C78.3324 9.7658 79.2072 10.9876 79.2702 13.0012C79.2863 13.5143 79.2729 14.0274 79.2729 14.5405L79.2702 14.5379ZM74.0426 15.6244C73.6956 15.7972 73.0177 15.9258 72.944 16.2112C72.6828 17.22 72.8797 17.721 74.0212 17.512C74.3668 17.4491 74.7138 17.3379 75.0286 17.1838C75.4359 16.9842 75.5833 16.2473 75.235 15.9941C74.9576 15.7932 74.5504 15.7704 74.0426 15.623V15.6244Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M36.6847 6.1808C36.6847 6.91899 36.6834 7.60224 36.6847 8.28549C36.6874 9.2032 36.7437 9.25813 37.6855 9.25947C38.3527 9.26081 39.0212 9.25947 39.7862 9.25947C39.7862 10.4116 39.8062 11.5343 39.758 12.6543C39.7527 12.7816 39.3628 12.9745 39.1391 12.9933C38.563 13.0415 37.9802 13.0187 37.4001 13.0067C36.8937 12.9959 36.6646 13.2197 36.6807 13.7328C36.6981 14.2901 36.6727 14.8488 36.6887 15.4047C36.7102 16.1282 36.9848 16.4202 37.7016 16.4604C38.1906 16.4886 38.6822 16.4725 39.1739 16.4698C39.5731 16.4685 39.8196 16.6373 39.8223 17.0566C39.829 18.0802 39.825 19.105 39.825 20.2304C37.802 20.1822 35.7576 20.6095 34.0227 19.2176C33.1144 18.4888 32.7567 17.4572 32.691 16.3291C32.6468 15.5735 32.6709 14.8126 32.6669 14.0543C32.6615 13.0991 32.5879 13.0241 31.6501 13.0053C31.4973 13.0026 31.3433 12.9852 31.0927 12.9665C31.0927 11.8706 31.0633 10.7854 31.1249 9.70693C31.1343 9.54349 31.5683 9.28358 31.8081 9.27956C32.4512 9.26885 32.695 8.99688 32.6709 8.38061C32.6495 7.84607 32.6133 7.30214 32.695 6.77831C32.7299 6.55458 33.0407 6.21295 33.239 6.20358C34.3469 6.14731 35.4602 6.17678 36.6861 6.17678L36.6847 6.1808Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M46.9683 20.2397C45.9474 20.2397 45.0766 20.2732 44.2098 20.2317C42.7214 20.162 41.7796 19.2631 41.5947 17.7894C41.5639 17.5469 41.5545 17.3004 41.5545 17.0552C41.5518 13.4206 41.5518 9.78594 41.5545 6.15129C41.5545 5.30191 41.6778 5.17732 42.5124 5.17464C43.5547 5.17062 44.5983 5.17464 45.7063 5.17464C45.7063 5.531 45.7063 5.79493 45.7063 6.05885C45.7063 9.27015 45.7143 12.4814 45.6982 15.6914C45.6956 16.2206 45.7746 16.5515 46.4177 16.5167C46.5945 16.5073 46.9388 16.7551 46.9442 16.8945C46.9884 17.9957 46.9683 19.0996 46.9683 20.2384V20.2397Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M65.088 9.26208C65.088 9.88237 65.076 10.3915 65.0961 10.8992C65.1014 11.0265 65.2126 11.1497 65.2756 11.2757C65.3801 11.1819 65.5194 11.1069 65.5824 10.9903C65.7833 10.6206 65.9146 10.2093 66.1477 9.86361C66.6073 9.18304 67.3106 8.97404 68.0823 9.09729C68.2819 9.12945 68.5793 9.43222 68.5874 9.62112C68.6343 10.7559 68.6075 11.8933 68.6128 13.0307C68.6155 13.5184 68.3275 13.4487 67.9831 13.4353C67.2383 13.4058 66.4867 13.3991 65.7458 13.4607C65.2635 13.5009 65.0639 13.8305 65.0733 14.3543C65.1041 16.0263 65.0719 17.6983 65.0907 19.3702C65.0974 19.9865 64.8777 20.2745 64.2319 20.2531C63.3196 20.2236 62.4046 20.2317 61.4909 20.2531C61.1104 20.2611 60.9751 20.1111 60.9751 19.7467C60.9805 16.4925 60.9764 13.237 60.9805 9.98285C60.9805 9.58763 61.1117 9.26744 61.5833 9.26476C62.7154 9.2594 63.8474 9.26342 65.0867 9.26342L65.088 9.26208Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function UnkeyLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="94"
|
||||
height="24"
|
||||
viewBox="0 0 96 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16.9012 20.0088C11.5775 20.0088 8.54297 17.1873 8.54297 12.3427V1.9082H11.3912V12.1564C11.3912 15.7499 12.9883 17.2405 16.9012 17.2405C20.8141 17.2405 22.4112 15.7499 22.4112 12.1564V1.9082H25.286V12.3427C25.286 17.1873 22.2515 20.0088 16.9012 20.0088ZM30.7083 19.7426H27.8335V6.51322H30.4688V10.6125H30.6551C31.0544 8.37651 32.8112 6.24703 36.1651 6.24703C39.8385 6.24703 41.6486 8.72256 41.6486 11.7837V19.7426H38.7738V12.5556C38.7738 10.0801 37.6558 8.82903 34.9141 8.82903C32.0127 8.82903 30.7083 10.3197 30.7083 13.1945V19.7426ZM47.085 19.7426H44.2102V1.9082H47.085V11.7571H50.8648L54.831 6.51322H58.1849L53.2072 12.8751L58.1583 19.7426H54.7777L50.8648 14.3391H47.085V19.7426ZM66.2537 20.0088C61.9149 20.0088 59.0667 17.5599 59.0667 13.1412C59.0667 9.01536 61.8883 6.24703 66.2005 6.24703C70.2997 6.24703 73.0947 8.50961 73.0947 12.529C73.0947 13.0081 73.0681 13.3808 72.9882 13.7801H61.7552C61.8616 16.3355 63.1127 17.693 66.1739 17.693C68.9422 17.693 70.0868 16.788 70.0868 15.2175V15.0045H72.9616V15.2441C72.9616 18.0657 70.1933 20.0088 66.2537 20.0088ZM66.1472 8.50961C63.2192 8.50961 61.9415 9.81392 61.7818 12.183H70.3796V12.1297C70.3796 9.68083 68.9688 8.50961 66.1472 8.50961ZM77.3773 24.2678H75.4874V21.6592H78.0694C79.2406 21.6592 79.7198 21.3398 80.1191 20.4347L80.4385 19.7426L73.9169 6.51322H77.1378L80.5183 13.5405L81.8226 16.7081H82.0356L83.2867 13.5139L86.401 6.51322H89.5686L82.6744 21.18C81.5831 23.5491 80.0924 24.2678 77.3773 24.2678Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
|
||||
{message && <Paragraph>{message}</Paragraph>}
|
||||
<LinkButton
|
||||
to={button ? button.to : "/"}
|
||||
shortcut={{ modifiers: ["meta"], key: "g" }}
|
||||
shortcut={{ modifiers: ["mod"], key: "g" }}
|
||||
variant="primary/medium"
|
||||
LeadingIcon={HomeIcon}
|
||||
>
|
||||
|
||||
@@ -30,10 +30,12 @@ function NextButton({ cursor }: { cursor?: string }) {
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path &&
|
||||
"cursor-not-allowed opacity-50 group-hover:bg-transparent group-hover:text-text-dimmed"
|
||||
!path && "cursor-not-allowed opacity-50 group-hover/button:bg-transparent"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
shortcut={{ key: "k" }}
|
||||
tooltip="Next"
|
||||
disabled={!path}
|
||||
>
|
||||
Next
|
||||
</LinkButton>
|
||||
@@ -51,10 +53,12 @@ function PreviousButton({ cursor }: { cursor?: string }) {
|
||||
leadingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path &&
|
||||
"cursor-not-allowed opacity-50 group-hover:bg-transparent group-hover:text-text-dimmed"
|
||||
!path && "cursor-not-allowed opacity-50 group-hover/button:bg-transparent"
|
||||
)}
|
||||
onClick={(e) => !path && e.preventDefault()}
|
||||
shortcut={{ key: "j" }}
|
||||
tooltip="Previous"
|
||||
disabled={!path}
|
||||
>
|
||||
Prev
|
||||
</LinkButton>
|
||||
|
||||
@@ -9,6 +9,10 @@ import { LinkButton } from "./primitives/Buttons";
|
||||
import { Header3 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { TextLink } from "./primitives/TextLink";
|
||||
import { TldrawLogo } from "~/assets/logos/TldrawLogo";
|
||||
import { UnkeyLogo } from "~/assets/logos/UnkeyLogo";
|
||||
import { LyftLogo } from "~/assets/logos/LyftLogo";
|
||||
import { MiddayLogo } from "~/assets/logos/MiddayLogo";
|
||||
|
||||
interface QuoteType {
|
||||
quote: string;
|
||||
@@ -72,11 +76,12 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
||||
<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 />
|
||||
<ATAndTLogo />
|
||||
<LyftLogo className="w-11" />
|
||||
<UnkeyLogo />
|
||||
<MiddayLogo />
|
||||
<AppsmithLogo />
|
||||
<CalComLogo />
|
||||
<TldrawLogo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { InlineCode } from "./code/InlineCode";
|
||||
import {
|
||||
@@ -8,7 +10,31 @@ import {
|
||||
} from "./primitives/ClientTabs";
|
||||
import { ClipboardField } from "./primitives/ClipboardField";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
|
||||
type PackageManagerContextType = {
|
||||
activePackageManager: string;
|
||||
setActivePackageManager: (value: string) => void;
|
||||
};
|
||||
|
||||
const PackageManagerContext = createContext<PackageManagerContextType | undefined>(undefined);
|
||||
|
||||
export function PackageManagerProvider({ children }: { children: React.ReactNode }) {
|
||||
const [activePackageManager, setActivePackageManager] = useState("npm");
|
||||
|
||||
return (
|
||||
<PackageManagerContext.Provider value={{ activePackageManager, setActivePackageManager }}>
|
||||
{children}
|
||||
</PackageManagerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function usePackageManager() {
|
||||
const context = useContext(PackageManagerContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("usePackageManager must be used within a PackageManagerProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey: string }) {
|
||||
return (
|
||||
@@ -131,7 +157,6 @@ export function TriggerDevStep({ extra }: { extra?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger.dev version 3 setup commands
|
||||
const v3PackageTag = "latest";
|
||||
|
||||
function getApiUrlArg() {
|
||||
@@ -160,14 +185,19 @@ function getApiUrlArg() {
|
||||
export function InitCommandV3() {
|
||||
const project = useProject();
|
||||
const projectRef = project.ref;
|
||||
|
||||
const apiUrlArg = getApiUrlArg();
|
||||
|
||||
const initCommandParts = [`trigger.dev@${v3PackageTag}`, "init", `-p ${projectRef}`, apiUrlArg];
|
||||
const initCommand = initCommandParts.filter(Boolean).join(" ");
|
||||
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabs
|
||||
defaultValue="npm"
|
||||
value={activePackageManager}
|
||||
onValueChange={setActivePackageManager}
|
||||
>
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
@@ -202,8 +232,14 @@ export function InitCommandV3() {
|
||||
}
|
||||
|
||||
export function TriggerDevStepV3() {
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabs
|
||||
defaultValue="npm"
|
||||
value={activePackageManager}
|
||||
onValueChange={setActivePackageManager}
|
||||
>
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
@@ -238,8 +274,14 @@ export function TriggerDevStepV3() {
|
||||
}
|
||||
|
||||
export function TriggerLoginStepV3() {
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabs
|
||||
defaultValue="npm"
|
||||
value={activePackageManager}
|
||||
onValueChange={setActivePackageManager}
|
||||
>
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Keyboard } from "lucide-react";
|
||||
import { Header3 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "./primitives/SheetV3";
|
||||
import { ShortcutKey } from "./primitives/ShortcutKey";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
|
||||
export function Shortcuts() {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={Keyboard}
|
||||
leadingIconClassName="text-blue-500"
|
||||
data-action="shortcuts"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
shortcut={{ modifiers: ["shift"], key: "?" }}
|
||||
className="gap-x-0 pl-0.5"
|
||||
iconSpacing="gap-x-0.5"
|
||||
>
|
||||
Shortcuts
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Keyboard className="size-5 text-indigo-500" />
|
||||
<span className="font-sans text-base font-medium text-text-bright">
|
||||
Keyboard shortcuts
|
||||
</span>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
<div className="space-y-6 px-4 pb-4 pt-2">
|
||||
<div className="space-y-3">
|
||||
<Header3>General</Header3>
|
||||
<Shortcut name="Close">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Confirm">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Filter">
|
||||
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Select filter">
|
||||
<ShortcutKey shortcut={{ key: "1" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Previous page">
|
||||
<ShortcutKey shortcut={{ key: "j" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Next page">
|
||||
<ShortcutKey shortcut={{ key: "k" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Help & Feedback">
|
||||
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Runs page</Header3>
|
||||
<Shortcut name="Bulk action: Cancel runs">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Replay runs">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Bulk action: Clear selection">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Run page</Header3>
|
||||
<Shortcut name="Replay run">
|
||||
<ShortcutKey shortcut={{ key: "r" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Overview">
|
||||
<ShortcutKey shortcut={{ key: "o" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Details">
|
||||
<ShortcutKey shortcut={{ key: "d" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Context">
|
||||
<ShortcutKey shortcut={{ key: "c" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Metadata">
|
||||
<ShortcutKey shortcut={{ key: "m" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Navigate">
|
||||
<ShortcutKey shortcut={{ key: "arrowup" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowdown" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Expand all">
|
||||
<ShortcutKey shortcut={{ key: "e" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Collapse all">
|
||||
<ShortcutKey shortcut={{ key: "w" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Toggle level">
|
||||
<ShortcutKey shortcut={{ key: "0" }} variant="medium/bright" />
|
||||
<Paragraph variant="small" className="ml-1.5">
|
||||
to
|
||||
</Paragraph>
|
||||
<ShortcutKey shortcut={{ key: "9" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Schedules page</Header3>
|
||||
<Shortcut name="New schedule">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Header3>Alerts page</Header3>
|
||||
<Shortcut name="New alert">
|
||||
<ShortcutKey shortcut={{ key: "n" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function Shortcut({ children, name }: { children: React.ReactNode; name: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<span className="text-sm text-text-dimmed">{name}</span>
|
||||
<span className="flex items-center gap-x-0.5">{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverSideMenuTrigger } from "../primitives/Popover";
|
||||
import { StepNumber } from "../primitives/StepNumber";
|
||||
import { MenuCount, SideMenuItem } from "./SideMenuItem";
|
||||
|
||||
import { Shortcuts } from "../Shortcuts";
|
||||
export function HelpAndFeedback() {
|
||||
const [isHelpMenuOpen, setHelpMenuOpen] = useState(false);
|
||||
const currentPlan = useCurrentPlan();
|
||||
@@ -84,6 +84,7 @@ export function HelpAndFeedback() {
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
<Shortcuts />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<Paragraph className="pb-1 pl-1.5 pt-1.5 text-xs">Need help?</Paragraph>
|
||||
|
||||
@@ -65,6 +65,7 @@ import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { FreePlanUsage } from "../billing/v2/FreePlanUsage";
|
||||
import { Badge } from "../primitives/Badge";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Popover,
|
||||
@@ -91,6 +92,34 @@ type SideMenuProps = {
|
||||
defaultValue?: FeedbackType;
|
||||
};
|
||||
|
||||
function V2Countdown() {
|
||||
const [days, setDays] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const targetDate = new Date("2025-01-31T00:00:00Z");
|
||||
|
||||
const calculateDays = () => {
|
||||
const now = new Date();
|
||||
const difference = targetDate.getTime() - now.getTime();
|
||||
return Math.floor(difference / (1000 * 60 * 60 * 24));
|
||||
};
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setDays(calculateDays());
|
||||
}, 1000 * 60 * 60); // Update every hour
|
||||
|
||||
setDays(calculateDays()); // Initial calculation
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Header2 className="flex-wrap gap-4 text-error">
|
||||
V2 goes offline in <span className="tabular-nums">{days}d</span>
|
||||
</Header2>
|
||||
);
|
||||
}
|
||||
|
||||
export function SideMenu({ user, project, organization, organizations }: SideMenuProps) {
|
||||
const borderRef = useRef<HTMLDivElement>(null);
|
||||
const [showHeaderDivider, setShowHeaderDivider] = useState(false);
|
||||
@@ -215,7 +244,8 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
</div>
|
||||
<div className="m-2">
|
||||
{project.version === "V2" && (
|
||||
<div className="flex flex-col gap-3 rounded border border-success/50 bg-success/10 p-3">
|
||||
<div className="flex flex-col gap-3 rounded border border-error/50 bg-error/5 p-3">
|
||||
<V2Countdown />
|
||||
<Paragraph variant="small/bright">
|
||||
This is a v2 project. V2 will be deprecated on January 31, 2025.{" "}
|
||||
<TextLink
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Link, LinkProps, NavLink, NavLinkProps } from "@remix-run/react";
|
||||
import React, { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import React, { forwardRef, ReactNode, useImperativeHandle, useRef } from "react";
|
||||
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
|
||||
|
||||
const sizes = {
|
||||
small: {
|
||||
@@ -65,8 +66,7 @@ const theme = {
|
||||
icon: "text-text-bright",
|
||||
},
|
||||
minimal: {
|
||||
textColor:
|
||||
"text-text-dimmed group-hover/button:text-text-bright transition group-disabled/button:text-text-dimmed/80",
|
||||
textColor: "text-text-dimmed group-disabled/button:text-text-dimmed transition",
|
||||
button:
|
||||
"bg-transparent group-hover/button:bg-tertiary disabled:opacity-50 group-disabled/button:bg-transparent group-disabled/button:pointer-events-none",
|
||||
shortcut:
|
||||
@@ -175,6 +175,8 @@ export type ButtonContentPropsType = {
|
||||
shortcut?: ShortcutDefinition;
|
||||
variant: keyof typeof variant;
|
||||
shortcutPosition?: "before-trailing-icon" | "after-trailing-icon";
|
||||
tooltip?: ReactNode;
|
||||
iconSpacing?: string;
|
||||
};
|
||||
|
||||
export function ButtonContent(props: ButtonContentPropsType) {
|
||||
@@ -188,23 +190,34 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
fullWidth,
|
||||
textAlignLeft,
|
||||
className,
|
||||
tooltip,
|
||||
iconSpacing,
|
||||
} = props;
|
||||
const variation = allVariants.variant[props.variant];
|
||||
|
||||
// Based on the size prop, we'll use the corresponding variant classnames
|
||||
const btnClassName = cn(allVariants.$all, variation.button);
|
||||
const iconClassName = variation.icon;
|
||||
const iconSpacingClassName = variation.iconSpacing;
|
||||
const shortcutClassName = variation.shortcut;
|
||||
const textColorClassName = variation.textColor;
|
||||
|
||||
return (
|
||||
const renderShortcutKey = () =>
|
||||
shortcut && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
);
|
||||
|
||||
const buttonContent = (
|
||||
<div className={cn("flex", fullWidth ? "" : "w-fit text-xxs", btnClassName, className)}>
|
||||
<div
|
||||
className={cn(
|
||||
textAlignLeft ? "text-left" : "justify-center",
|
||||
"flex w-full items-center",
|
||||
iconSpacingClassName
|
||||
iconSpacingClassName,
|
||||
iconSpacing
|
||||
)}
|
||||
>
|
||||
{LeadingIcon &&
|
||||
@@ -238,13 +251,10 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
<>{text}</>
|
||||
))}
|
||||
|
||||
{shortcut && props.shortcutPosition === "before-trailing-icon" && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
{shortcut &&
|
||||
!tooltip &&
|
||||
props.shortcutPosition === "before-trailing-icon" &&
|
||||
renderShortcutKey()}
|
||||
|
||||
{TrailingIcon &&
|
||||
(typeof TrailingIcon === "string" ? (
|
||||
@@ -269,16 +279,27 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
))}
|
||||
|
||||
{shortcut &&
|
||||
(!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
!tooltip &&
|
||||
(!props.shortcutPosition || props.shortcutPosition === "after-trailing-icon") &&
|
||||
renderShortcutKey()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{buttonContent}</TooltipTrigger>
|
||||
<TooltipContent className="text-dimmed flex items-center gap-3 py-1.5 pl-2.5 pr-3 text-xs">
|
||||
{tooltip} {shortcut && renderShortcutKey()}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return buttonContent;
|
||||
}
|
||||
|
||||
type ButtonPropsType = Pick<
|
||||
|
||||
@@ -5,7 +5,11 @@ import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const ClientTabs = TabsPrimitive.Root;
|
||||
const ClientTabs = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
|
||||
>((props, ref) => <TabsPrimitive.Root ref={ref} {...props} />);
|
||||
ClientTabs.displayName = TabsPrimitive.Root.displayName;
|
||||
|
||||
const ClientTabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cn } from "~/utils/cn";
|
||||
import { Icon, RenderIcon } from "./Icon";
|
||||
|
||||
const containerBase =
|
||||
"has-[:focus-visible]:outline-none has-[:focus-visible]:ring-1 has-[:focus-visible]:ring-text-link has-[:focus-visible]:ring-offset-0 has-[:focus]:border-ring has-[:focus]:outline-none has-[:focus]:ring-2 has-[:focus]:ring-ring has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 ring-offset-background transition cursor-text";
|
||||
"has-[:focus-visible]:outline-none has-[:focus-visible]:ring-1 has-[:focus-visible]:ring-charcoal-650 has-[:focus-visible]:ring-offset-0 has-[:focus]:border-ring has-[:focus]:outline-none has-[:focus]:ring-1 has-[:focus]:ring-ring has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 ring-offset-background transition cursor-text";
|
||||
|
||||
const inputBase =
|
||||
"h-full w-full text-text-bright bg-transparent file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed outline-none ring-0 border-none";
|
||||
@@ -17,27 +17,27 @@ const variants = {
|
||||
container:
|
||||
"px-1 w-full h-10 rounded-[3px] border border-charcoal-800 bg-charcoal-750 hover:border-charcoal-600 hover:bg-charcoal-650",
|
||||
input: "px-2 text-sm",
|
||||
iconSize: "h-4 w-4 ml-1",
|
||||
iconSize: "size-4 ml-1",
|
||||
shortcut: "mr-1 min-w-[22px] rounded-sm py-[3px] px-[5px] text-[0.6rem] select-none",
|
||||
},
|
||||
medium: {
|
||||
container:
|
||||
"px-1 h-8 w-full rounded border border-charcoal-800 bg-charcoal-750 hover:border-charcoal-600 hover:bg-charcoal-650",
|
||||
input: "px-1.5 rounded text-sm",
|
||||
iconSize: "h-4 w-4 ml-0.5",
|
||||
iconSize: "size-4 ml-0.5",
|
||||
shortcut: "min-w-[22px] rounded-sm py-[3px] px-[5px] text-[0.6rem]",
|
||||
},
|
||||
small: {
|
||||
container:
|
||||
"px-1 h-6 w-full rounded border border-charcoal-800 bg-charcoal-750 hover:border-charcoal-600 hover:bg-charcoal-650",
|
||||
input: "px-1 rounded text-xs",
|
||||
iconSize: "h-3 w-3 ml-0.5",
|
||||
iconSize: "size-3 ml-0.5",
|
||||
shortcut: "min-w-[22px] rounded-[2px] py-px px-[3px] text-[0.5rem]",
|
||||
},
|
||||
tertiary: {
|
||||
container: "px-1 h-6 w-full rounded hover:bg-charcoal-750",
|
||||
input: "px-1 rounded text-xs",
|
||||
iconSize: "h-3 w-3 ml-0.5",
|
||||
iconSize: "size-3 ml-0.5",
|
||||
shortcut: "min-w-[22px] rounded-[2px] py-px px-[3px] text-[0.5rem]",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type Props = Parameters<typeof Link>[0] & {
|
||||
disabled?: boolean;
|
||||
disabledClassName?: string;
|
||||
};
|
||||
|
||||
export function LinkDisabled({ disabled = false, disabledClassName, ...props }: Props) {
|
||||
if (disabled) {
|
||||
return (
|
||||
<span {...props} className={cn(props.className, disabledClassName)}>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return <Link {...props}>{props.children}</Link>;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { Link, useLocation } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ButtonContent } from "./Buttons";
|
||||
import { LinkDisabled } from "./LinkWithDisabled";
|
||||
import { LinkButton } from "./Buttons";
|
||||
|
||||
export function PaginationControls({
|
||||
currentPage,
|
||||
@@ -21,16 +20,17 @@ export function PaginationControls({
|
||||
|
||||
return (
|
||||
<nav className="flex items-center gap-0.5" aria-label="Pagination">
|
||||
<LinkDisabled
|
||||
<LinkButton
|
||||
to={pageUrl(location, currentPage - 1)}
|
||||
className={currentPage > 1 ? "group" : ""}
|
||||
variant="minimal/small"
|
||||
LeadingIcon={ChevronLeftIcon}
|
||||
shortcut={{ key: "j" }}
|
||||
tooltip="Previous"
|
||||
disabled={currentPage === 1}
|
||||
disabledClassName="opacity-30 cursor-default"
|
||||
className={currentPage > 1 ? "group" : ""}
|
||||
>
|
||||
<ButtonContent variant="minimal/small" LeadingIcon={ChevronLeftIcon}>
|
||||
Prev
|
||||
</ButtonContent>
|
||||
</LinkDisabled>
|
||||
Prev
|
||||
</LinkButton>
|
||||
|
||||
{showPageNumbers
|
||||
? calculatePageLinks(currentPage, totalPages).map((page, i) => (
|
||||
@@ -38,16 +38,17 @@ export function PaginationControls({
|
||||
))
|
||||
: null}
|
||||
|
||||
<LinkDisabled
|
||||
<LinkButton
|
||||
to={pageUrl(location, currentPage + 1)}
|
||||
className={currentPage !== totalPages ? "group" : ""}
|
||||
variant="minimal/small"
|
||||
TrailingIcon={ChevronRightIcon}
|
||||
shortcut={{ key: "k" }}
|
||||
tooltip="Next"
|
||||
disabled={currentPage === totalPages}
|
||||
disabledClassName="opacity-30 cursor-default"
|
||||
className={currentPage !== totalPages ? "group" : ""}
|
||||
>
|
||||
<ButtonContent variant="minimal/small" TrailingIcon={ChevronRightIcon}>
|
||||
Next
|
||||
</ButtonContent>
|
||||
</LinkDisabled>
|
||||
Next
|
||||
</LinkButton>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -61,7 +62,7 @@ function pageUrl(location: ReturnType<typeof useLocation>, page: number): string
|
||||
}
|
||||
|
||||
const baseClass =
|
||||
"flex items-center justify-center border border-transparent min-w-6 h-6 px-1 text-xs font-medium transition text-text-dimmed rounded-sm";
|
||||
"flex items-center justify-center border border-transparent min-w-6 h-6 px-1 text-xs font-medium transition text-text-dimmed rounded-sm focus-visible:focus-custom";
|
||||
const unselectedClass = "hover:bg-tertiary hover:text-text-bright";
|
||||
const selectedClass = "border-charcoal-600 bg-tertiary text-text-bright hover:bg-charcoal-600/50";
|
||||
|
||||
|
||||
@@ -482,7 +482,11 @@ export function SelectItem({
|
||||
<div className="grow truncate">{props.children || props.value}</div>
|
||||
{checkIcon}
|
||||
{shortcut && (
|
||||
<ShortcutKey className={cn("size-4 flex-none")} shortcut={shortcut} variant={"small"} />
|
||||
<ShortcutKey
|
||||
className={cn("size-4 flex-none transition duration-0 group-hover:border-charcoal-600")}
|
||||
shortcut={shortcut}
|
||||
variant={"small"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Ariakit.SelectItem>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Header2 } from "./Headers";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 overflow-y-auto bg-background-dimmed shadow-lg transition ease-in-out border-grid-dimmed data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"sticky top-0 flex items-center justify-between border-b border-grid-bright bg-background-dimmed pb-1.5 pl-3 pr-1.5 pt-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="flex items-center gap-1 rounded-sm p-1 pl-0 transition hover:bg-charcoal-750 focus-visible:focus-custom disabled:pointer-events-none">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
|
||||
<NamedIcon name="close" className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Title>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("overflow-y-auto px-3 py-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
||||
@@ -1,25 +1,35 @@
|
||||
import { Fragment } from "react";
|
||||
import { Modifier, ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
import { KeyboardDownIcon } from "~/assets/icons/KeyboardDownIcon";
|
||||
import { KeyboardLeftIcon } from "~/assets/icons/KeyboardLeftIcon";
|
||||
import { KeyboardRightIcon } from "~/assets/icons/KeyboardRightIcon";
|
||||
import { KeyboardUpIcon } from "~/assets/icons/KeyboardUpIcon";
|
||||
import { KeyboardWindowsIcon } from "~/assets/icons/KeyboardWindowsIcon";
|
||||
import { Modifier, Shortcut } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useOperatingSystem } from "./OperatingSystemProvider";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
|
||||
const medium =
|
||||
"text-[0.75rem] font-medium min-w-[17px] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1.5 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase";
|
||||
|
||||
export const variants = {
|
||||
small:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase",
|
||||
medium:
|
||||
"text-[0.75rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase",
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase",
|
||||
medium,
|
||||
"medium/bright": cn(medium, "bg-charcoal-750 text-text-bright border-charcoal-650"),
|
||||
};
|
||||
|
||||
export type ShortcutKeyVariant = keyof typeof variants;
|
||||
|
||||
type ShortcutKey = Partial<Shortcut>;
|
||||
|
||||
type ShortcutKeyDefinition =
|
||||
| {
|
||||
windows: ShortcutKey;
|
||||
mac: ShortcutKey;
|
||||
}
|
||||
| ShortcutKey;
|
||||
|
||||
type ShortcutKeyProps = {
|
||||
shortcut: ShortcutDefinition;
|
||||
shortcut: ShortcutKeyDefinition;
|
||||
variant: ShortcutKeyVariant;
|
||||
className?: string;
|
||||
};
|
||||
@@ -29,50 +39,62 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
const isMac = platform === "mac";
|
||||
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const modifiers = relevantShortcut.modifiers ?? [];
|
||||
const character = keyString(relevantShortcut.key, isMac, variant);
|
||||
const character = relevantShortcut.key ? keyString(relevantShortcut.key, isMac, variant) : null;
|
||||
|
||||
return (
|
||||
<span className={cn(variants[variant], className)}>
|
||||
{modifiers.map((k) => (
|
||||
<Fragment key={k}>{modifierString(k, isMac)}</Fragment>
|
||||
<span key={k}>
|
||||
<span>{modifierString(k, isMac, variant)}</span>
|
||||
</span>
|
||||
))}
|
||||
{character}
|
||||
{character && <span>{character}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function keyString(key: String, isMac: boolean, size: "small" | "medium") {
|
||||
function keyString(key: string, isMac: boolean, variant: "small" | "medium" | "medium/bright") {
|
||||
key = key.toLowerCase();
|
||||
|
||||
const className = size === "small" ? "w-2.5 h-4" : "w-3 h-5";
|
||||
const className = variant === "small" ? "w-2.5 h-4" : "w-3 h-5";
|
||||
|
||||
switch (key) {
|
||||
case "enter":
|
||||
return isMac ? "↵" : key;
|
||||
return isMac ? "↵" : <span className="capitalize">Enter</span>;
|
||||
case "esc":
|
||||
return <span className="capitalize">Esc</span>;
|
||||
case "del":
|
||||
return <span className="capitalize">Del</span>;
|
||||
case "arrowdown":
|
||||
return <ChevronDownIcon className={className} />;
|
||||
return <KeyboardDownIcon className={className} />;
|
||||
case "arrowup":
|
||||
return <ChevronUpIcon className={className} />;
|
||||
return <KeyboardUpIcon className={className} />;
|
||||
case "arrowleft":
|
||||
return <ChevronLeftIcon className={className} />;
|
||||
return <KeyboardLeftIcon className={className} />;
|
||||
case "arrowright":
|
||||
return <ChevronRightIcon className={className} />;
|
||||
return <KeyboardRightIcon className={className} />;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
function modifierString(modifier: Modifier, isMac: boolean) {
|
||||
function modifierString(
|
||||
modifier: Modifier,
|
||||
isMac: boolean,
|
||||
variant: "small" | "medium" | "medium/bright"
|
||||
): string | JSX.Element {
|
||||
const className = variant === "small" ? "w-2.5 h-4" : "w-3.5 h-5";
|
||||
|
||||
switch (modifier) {
|
||||
case "alt":
|
||||
return isMac ? "⌥" : "Alt+";
|
||||
return isMac ? "⌥" : <span className="capitalize">Alt</span>;
|
||||
case "ctrl":
|
||||
return isMac ? "⌃" : "Ctrl+";
|
||||
return isMac ? "⌃" : <span className="capitalize">Ctrl</span>;
|
||||
case "meta":
|
||||
return isMac ? "⌘" : "⊞+";
|
||||
return isMac ? "⌘" : <KeyboardWindowsIcon className={className} />;
|
||||
case "shift":
|
||||
return isMac ? "⇧" : "Shift+";
|
||||
return "⇧";
|
||||
case "mod":
|
||||
return isMac ? "⌘" : "Ctrl+";
|
||||
return isMac ? "⌘" : <span className="capitalize">Ctrl</span>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:ring-ring group flex items-center justify-between gap-x-1 rounded text-text-dimmed transition placeholder:text-text-dimmed hover:text-text-bright focus-visible:bg-tertiary focus-visible:text-text-bright focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"ring-offset-background group flex items-center justify-between gap-x-1 rounded text-text-dimmed transition placeholder:text-text-dimmed hover:text-text-bright focus-visible:focus-custom disabled:cursor-not-allowed disabled:opacity-50",
|
||||
width === "full" ? "w-full" : "w-min",
|
||||
sizeClassName,
|
||||
className
|
||||
|
||||
@@ -26,8 +26,8 @@ export function Spinner({
|
||||
foreground: "#3C4B62",
|
||||
},
|
||||
dark: {
|
||||
background: "#15171A",
|
||||
foreground: "#272A2E",
|
||||
background: "rgba(18, 19, 23, 0.35)",
|
||||
foreground: "#1A1B1F",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ const variations = {
|
||||
large: {
|
||||
container: "flex items-center gap-x-2 rounded-md hover:bg-tertiary p-2 transition focus-custom",
|
||||
root: "h-6 w-11",
|
||||
thumb: "h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-sm text-charcoal-400 group-hover:text-charcoal-200 transition",
|
||||
thumb: "size-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-sm text-text-dimmed",
|
||||
},
|
||||
small: {
|
||||
container:
|
||||
"flex items-center h-[1.5rem] gap-x-1.5 rounded hover:bg-tertiary disabled:hover:bg-transparent pr-1 py-[0.1rem] pl-1.5 transition focus-custom disabled:hover:text-charcoal-400 disabled:opacity-50 text-charcoal-400 hover:text-charcoal-200 disabled:hover:cursor-not-allowed hover:cursor-pointer",
|
||||
root: "h-3 w-6",
|
||||
thumb: "h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-xs",
|
||||
thumb: "size-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-xs text-text-dimmed",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors group-disabled:cursor-not-allowed group-disabled:opacity-50 group-data-[state=checked]:bg-text-link group-data-[state=unchecked]:bg-charcoal-700",
|
||||
"inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors group-disabled:cursor-not-allowed group-disabled:opacity-50 group-data-[state=checked]:bg-blue-500 group-data-[state=unchecked]:bg-charcoal-700 group-data-[state=unchecked]:group-hover:bg-charcoal-500/50",
|
||||
root
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { ReactNode, forwardRef, useState } from "react";
|
||||
import React, { ReactNode, forwardRef, useState, useContext, createContext } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover";
|
||||
import { InfoIconTooltip } from "./Tooltip";
|
||||
|
||||
const variants = {
|
||||
bright: {
|
||||
header: "bg-background-bright",
|
||||
cell: "group-hover/table-row:bg-charcoal-750 group-has-[[tabindex='0']:focus]/table-row:bg-charcoal-750",
|
||||
stickyCell: "bg-background-bright group-hover/table-row:bg-charcoal-750",
|
||||
menuButton:
|
||||
"bg-background-bright group-hover/table-row:bg-charcoal-750 group-hover/table-row:ring-charcoal-600/70 group-has-[[tabindex='0']:focus]/table-row:bg-charcoal-750",
|
||||
menuButtonDivider: "group-hover/table-row:border-charcoal-600/70",
|
||||
rowSelected: "bg-charcoal-750 group-hover/table-row:bg-charcoal-750",
|
||||
},
|
||||
dimmed: {
|
||||
header: "bg-background-dimmed",
|
||||
cell: "group-hover/table-row:bg-charcoal-800 group-has-[[tabindex='0']:focus]/table-row:bg-background-bright",
|
||||
stickyCell: "group-hover/table-row:bg-charcoal-800",
|
||||
menuButton:
|
||||
"bg-background-dimmed group-hover/table-row:bg-charcoal-800 group-hover/table-row:ring-grid-bright group-has-[[tabindex='0']:focus]/table-row:bg-background-bright",
|
||||
menuButtonDivider: "group-hover/table-row:border-grid-dimmed",
|
||||
rowSelected: "bg-charcoal-750 group-hover/table-row:bg-charcoal-750",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type TableVariant = keyof typeof variants;
|
||||
|
||||
type TableProps = {
|
||||
containerClassName?: string;
|
||||
className?: string;
|
||||
@@ -12,20 +35,25 @@ type TableProps = {
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
export const Table = forwardRef<HTMLTableElement, TableProps>(
|
||||
({ className, containerClassName, children, fullWidth }, ref) => {
|
||||
// Add TableContext
|
||||
const TableContext = createContext<{ variant: TableVariant }>({ variant: "dimmed" });
|
||||
|
||||
export const Table = forwardRef<HTMLTableElement, TableProps & { variant?: TableVariant }>(
|
||||
({ className, containerClassName, children, fullWidth, variant = "dimmed" }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto whitespace-nowrap border-t border-grid-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
containerClassName,
|
||||
fullWidth && "w-full"
|
||||
)}
|
||||
>
|
||||
<table ref={ref} className={cn("w-full", className)}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
<TableContext.Provider value={{ variant }}>
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto whitespace-nowrap border-t scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
containerClassName,
|
||||
fullWidth && "w-full"
|
||||
)}
|
||||
>
|
||||
<table ref={ref} className={cn("w-full", className)}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</TableContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -37,11 +65,13 @@ type TableHeaderProps = {
|
||||
|
||||
export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>(
|
||||
({ className, children }, ref) => {
|
||||
const { variant } = useContext(TableContext);
|
||||
return (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"sticky top-0 z-10 bg-background-dimmed after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-grid-bright",
|
||||
"sticky top-0 z-10 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-grid-bright",
|
||||
variants[variant].header,
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -70,16 +100,19 @@ type TableRowProps = {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
};
|
||||
|
||||
export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
|
||||
({ className, disabled, children }, ref) => {
|
||||
({ className, disabled, isSelected, children }, ref) => {
|
||||
const { variant } = useContext(TableContext);
|
||||
return (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group/table-row relative w-full outline-none after:absolute after:bottom-0 after:left-3 after:right-0 after:h-px after:bg-grid-dimmed",
|
||||
isSelected && variants[variant].rowSelected,
|
||||
disabled && "opacity-50",
|
||||
"group/table-row relative w-full after:absolute after:bottom-0 after:left-3 after:right-0 after:h-px after:bg-grid-dimmed",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -92,7 +125,7 @@ export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
|
||||
type TableCellBasicProps = {
|
||||
className?: string;
|
||||
alignment?: "left" | "center" | "right";
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
colSpan?: number;
|
||||
};
|
||||
|
||||
@@ -123,6 +156,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{hiddenLabel ? (
|
||||
<span className="sr-only">{children}</span>
|
||||
@@ -145,21 +179,12 @@ type TableCellProps = TableCellBasicProps & {
|
||||
hasAction?: boolean;
|
||||
isSticky?: boolean;
|
||||
actionClassName?: string;
|
||||
rowHoverStyle?: keyof typeof rowHoverStyles;
|
||||
rowHoverStyle?: string;
|
||||
isSelected?: boolean;
|
||||
isTabbableCell?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const rowHoverStyles = {
|
||||
default:
|
||||
"group-hover/table-row:bg-charcoal-800 group-hover/table-row:before:absolute group-hover/table-row:before:bg-charcoal-750 group-hover/table-row:before:top-[-1px] group-hover/table-row:before:left-0 group-hover/table-row:before:h-px group-hover/table-row:before:w-3 group-hover/table-row:after:absolute group-hover/table-row:after:bg-charcoal-750 group-hover/table-row:after:bottom-0 group-hover/table-row:after:left-0 group-hover/table-row:after:h-px group-hover/table-row:after:w-3",
|
||||
dimmed:
|
||||
"group-hover/table-row:bg-charcoal-850 group-hover/table-row:before:absolute group-hover/table-row:before:bg-charcoal-800 group-hover/table-row:before:top-[-1px] group-hover/table-row:before:left-0 group-hover/table-row:before:h-px group-hover/table-row:before:w-3 group-hover/table-row:after:absolute group-hover/table-row:after:bg-charcoal-800 group-hover/table-row:after:bottom-0 group-hover/table-row:after:left-0 group-hover/table-row:after:h-px group-hover/table-row:after:w-3",
|
||||
bright:
|
||||
"group-hover/table-row:bg-charcoal-750 group-hover/table-row:before:absolute group-hover/table-row:before:bg-charcoal-700 group-hover/table-row:before:top-[-1px] group-hover/table-row:before:left-0 group-hover/table-row:before:h-px group-hover/table-row:before:w-3 group-hover/table-row:after:absolute group-hover/table-row:after:bg-charcoal-700 group-hover/table-row:after:bottom-0 group-hover/table-row:after:left-0 group-hover/table-row:after:h-px group-hover/table-row:after:w-3",
|
||||
};
|
||||
|
||||
const stickyStyles =
|
||||
"sticky right-0 bg-background-dimmed group-hover/table-row:bg-charcoal-750 w-[--sticky-width] [&:has(.group-hover\\/table-row\\:block)]:w-auto";
|
||||
|
||||
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
(
|
||||
{
|
||||
@@ -172,7 +197,8 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
onClick,
|
||||
hasAction = false,
|
||||
isSticky = false,
|
||||
rowHoverStyle = "default",
|
||||
isSelected,
|
||||
isTabbableCell = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -187,33 +213,47 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
}
|
||||
|
||||
const flexClasses = cn(
|
||||
"flex w-full whitespace-nowrap px-3 py-3 text-xs text-text-dimmed",
|
||||
"flex w-full whitespace-nowrap px-3 py-3 items-center text-xs text-text-dimmed",
|
||||
alignment === "left"
|
||||
? "justify-start text-left"
|
||||
: alignment === "center"
|
||||
? "justify-center text-center"
|
||||
: "justify-end text-right"
|
||||
);
|
||||
const { variant } = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-xs text-charcoal-400",
|
||||
to || onClick || hasAction ? "cursor-pointer" : "px-3 py-3 align-middle",
|
||||
"text-xs text-charcoal-400 has-[[tabindex='0']:focus]:before:absolute has-[[tabindex='0']:focus]:before:-top-px has-[[tabindex='0']:focus]:before:left-0 has-[[tabindex='0']:focus]:before:h-px has-[[tabindex='0']:focus]:before:w-3 has-[[tabindex='0']:focus]:before:bg-grid-dimmed has-[[tabindex='0']:focus]:after:absolute has-[[tabindex='0']:focus]:after:bottom-0 has-[[tabindex='0']:focus]:after:left-0 has-[[tabindex='0']:focus]:after:right-0 has-[[tabindex='0']:focus]:after:h-px has-[[tabindex='0']:focus]:after:bg-grid-dimmed",
|
||||
variants[variant].cell,
|
||||
to || onClick || hasAction ? "cursor-pointer" : "cursor-default px-3 py-3 align-middle",
|
||||
!to && !onClick && alignmentClassName,
|
||||
isSticky && stickyStyles,
|
||||
rowHoverStyles[rowHoverStyle],
|
||||
isSticky &&
|
||||
"[&:has(.group-hover/table-row:block)]:w-auto sticky right-0 bg-background-dimmed",
|
||||
isSticky && variants[variant].stickyCell,
|
||||
isSelected && variants[variant].rowSelected,
|
||||
!isSelected &&
|
||||
"group-hover/table-row:before:absolute group-hover/table-row:before:left-0 group-hover/table-row:before:top-[-1px] group-hover/table-row:before:h-px group-hover/table-row:before:w-3 group-hover/table-row:before:bg-charcoal-750 group-hover/table-row:after:absolute group-hover/table-row:after:bottom-0 group-hover/table-row:after:left-0 group-hover/table-row:after:h-px group-hover/table-row:after:w-3 group-hover/table-row:after:bg-charcoal-750 group-focus-visible/table-row:bg-background-bright",
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
>
|
||||
{to ? (
|
||||
<Link to={to} className={cn("focus-custom", flexClasses, actionClassName)}>
|
||||
<Link
|
||||
to={to}
|
||||
className={cn("cursor-pointer focus:outline-none", flexClasses, actionClassName)}
|
||||
tabIndex={isTabbableCell ? 0 : -1}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
) : onClick ? (
|
||||
<button onClick={onClick} className={cn("focus-custom", flexClasses, actionClassName)}>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn("cursor-pointer focus:outline-none", flexClasses, actionClassName)}
|
||||
tabIndex={isTabbableCell ? 0 : -1}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
@@ -251,7 +291,7 @@ export const TableCellChevron = forwardRef<
|
||||
|
||||
export const TableCellMenu = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
{
|
||||
TableCellProps & {
|
||||
className?: string;
|
||||
isSticky?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
@@ -259,13 +299,24 @@ export const TableCellMenu = forwardRef<
|
||||
hiddenButtons?: ReactNode;
|
||||
popoverContent?: ReactNode;
|
||||
children?: ReactNode;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, isSticky, onClick, visibleButtons, hiddenButtons, popoverContent, children },
|
||||
{
|
||||
className,
|
||||
isSticky,
|
||||
onClick,
|
||||
visibleButtons,
|
||||
hiddenButtons,
|
||||
popoverContent,
|
||||
children,
|
||||
isSelected,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { variant } = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
@@ -275,12 +326,23 @@ export const TableCellMenu = forwardRef<
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
hasAction={true}
|
||||
isSelected={isSelected}
|
||||
>
|
||||
<div className="relative p-1">
|
||||
<div className="absolute right-0 top-1/2 mr-1 flex -translate-y-1/2 items-center justify-end gap-0.5 bg-background-dimmed p-0.5 group-hover/table-row:rounded-[0.25rem] group-hover/table-row:bg-background-bright group-hover/table-row:ring-1 group-hover/table-row:ring-grid-bright">
|
||||
<div className="relative h-full p-1">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 top-1/2 mr-1 flex -translate-y-1/2 items-center justify-end gap-0.5 rounded-[0.25rem] p-0.5 group-hover/table-row:ring-1",
|
||||
variants[variant].menuButton
|
||||
)}
|
||||
>
|
||||
{/* Hidden buttons that show on hover */}
|
||||
{hiddenButtons && (
|
||||
<div className="hidden pr-0.5 group-hover/table-row:block group-hover/table-row:border-r group-hover/table-row:border-grid-dimmed">
|
||||
<div
|
||||
className={cn(
|
||||
"hidden pr-0.5 group-hover/table-row:block group-hover/table-row:border-r",
|
||||
variants[variant].menuButtonDivider
|
||||
)}
|
||||
>
|
||||
{hiddenButtons}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { cn } from "~/utils/cn";
|
||||
|
||||
const variations = {
|
||||
primary:
|
||||
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group",
|
||||
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group focus-visible:focus-custom",
|
||||
secondary:
|
||||
"text-text-dimmed transition underline underline-offset-2 decoration-dimmed/50 hover:decoration-dimmed inline-flex gap-0.5 items-center group",
|
||||
"text-text-dimmed transition underline underline-offset-2 decoration-dimmed/50 hover:decoration-dimmed inline-flex gap-0.5 items-center group focus-visible:focus-custom",
|
||||
} as const;
|
||||
|
||||
type TextLinkProps = {
|
||||
@@ -34,14 +34,14 @@ export function TextLink({
|
||||
<Link to={to} className={cn(classes, className)} {...props}>
|
||||
{children}{" "}
|
||||
{trailingIcon && (
|
||||
<NamedIcon name={trailingIcon} className={cn("h-4 w-4", trailingIconClassName)} />
|
||||
<NamedIcon name={trailingIcon} className={cn("size-4", trailingIconClassName)} />
|
||||
)}
|
||||
</Link>
|
||||
) : href ? (
|
||||
<a href={href} className={cn(classes, className)} {...props}>
|
||||
{children}{" "}
|
||||
{trailingIcon && (
|
||||
<NamedIcon name={trailingIcon} className={cn("h-4 w-4", trailingIconClassName)} />
|
||||
<NamedIcon name={trailingIcon} className={cn("size-4", trailingIconClassName)} />
|
||||
)}
|
||||
</a>
|
||||
) : (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTypedLoaderData } from "remix-typedjson";
|
||||
import { loader } from "~/root";
|
||||
import { useEffect } from "react";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const defaultToastDuration = 5000;
|
||||
const permanentToastDuration = 60 * 60 * 24 * 1000;
|
||||
@@ -39,23 +40,27 @@ export function ToastUI({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`self-end rounded-md border border-grid-bright bg-background-dimmed`}
|
||||
className={cn(
|
||||
"self-end rounded-md border border-grid-bright bg-background-dimmed",
|
||||
variant === "success" && "border-success",
|
||||
variant === "error" && "border-error"
|
||||
)}
|
||||
style={{
|
||||
width: toastWidth,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-start gap-2 rounded-lg p-3">
|
||||
{variant === "success" ? (
|
||||
<CheckCircleIcon className="mt-1 h-6 min-h-[1.5rem] w-6 min-w-[1.5rem] text-green-600" />
|
||||
<CheckCircleIcon className="mt-1 size-6 min-w-6 text-success" />
|
||||
) : (
|
||||
<ExclamationCircleIcon className="mt-1 h-6 w-6 min-w-[1.5rem] text-rose-600" />
|
||||
<ExclamationCircleIcon className="mt-1 size-6 min-w-6 text-error" />
|
||||
)}
|
||||
<Paragraph className="py-1 text-text-dimmed">{message}</Paragraph>
|
||||
<Paragraph className="py-1 text-text-bright">{message}</Paragraph>
|
||||
<button
|
||||
className="hover:bg-midnight-800 ms-auto rounded p-2 text-text-dimmed transition hover:text-text-bright"
|
||||
onClick={() => toast.dismiss(t)}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
<XMarkIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -400,7 +400,7 @@ function BatchIdDropdown({
|
||||
disabled={error !== undefined || !batchId}
|
||||
variant="secondary/small"
|
||||
shortcut={{
|
||||
modifiers: ["meta"],
|
||||
modifiers: ["mod"],
|
||||
key: "Enter",
|
||||
enabledOnInputElements: true,
|
||||
}}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialog
|
||||
variant="danger/medium"
|
||||
LeadingIcon={isLoading ? "spinner-white" : NoSymbolIcon}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
shortcut={{ modifiers: ["mod"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Canceling..." : "Cancel run"}
|
||||
</Button>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function CheckBatchCompletionDialog({
|
||||
variant="primary/medium"
|
||||
LeadingIcon={isLoading ? "spinner-white" : undefined}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
shortcut={{ modifiers: ["mod"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Attempting resume..." : "Attempt resume"}
|
||||
</Button>
|
||||
|
||||
@@ -118,3 +118,34 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus, isBuilt: b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PENDING and CANCELED are not used so are ommited from the UI
|
||||
export const deploymentStatuses: WorkerDeploymentStatus[] = [
|
||||
"BUILDING",
|
||||
"DEPLOYING",
|
||||
"DEPLOYED",
|
||||
"FAILED",
|
||||
"TIMED_OUT",
|
||||
];
|
||||
|
||||
export function deploymentStatusDescription(status: WorkerDeploymentStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "The deployment is queued and waiting to be processed.";
|
||||
case "BUILDING":
|
||||
return "The code is being built and prepared for deployment.";
|
||||
case "DEPLOYING":
|
||||
return "The deployment is in progress and tasks are being indexed.";
|
||||
case "DEPLOYED":
|
||||
return "The deployment has completed successfully.";
|
||||
case "CANCELED":
|
||||
return "The deployment was manually canceled.";
|
||||
case "FAILED":
|
||||
return "The deployment encountered an error and could not complete.";
|
||||
case "TIMED_OUT":
|
||||
return "The deployment exceeded the maximum allowed time and was stopped.";
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ function ReplayForm({
|
||||
variant="primary/medium"
|
||||
LeadingIcon={isSubmitting ? ButtonSpinner : undefined}
|
||||
disabled={isSubmitting}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter", enabledOnInputElements: true }}
|
||||
shortcut={{ modifiers: ["mod"], key: "enter", enabledOnInputElements: true }}
|
||||
>
|
||||
{isSubmitting ? "Replaying..." : "Replay run"}
|
||||
</Button>
|
||||
|
||||
@@ -48,7 +48,7 @@ export function RetryDeploymentIndexingDialog({
|
||||
variant="primary/medium"
|
||||
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
shortcut={{ modifiers: ["mod"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Retrying..." : "Retry indexing"}
|
||||
</Button>
|
||||
|
||||
@@ -48,7 +48,7 @@ export function RollbackDeploymentDialog({
|
||||
variant="primary/medium"
|
||||
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
shortcut={{ modifiers: ["mod"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Rolling back..." : "Roll back deployment"}
|
||||
</Button>
|
||||
|
||||
@@ -356,7 +356,7 @@ function StatusDropdown({
|
||||
<TooltipTrigger className="group flex w-full flex-col py-0">
|
||||
<TaskRunStatusCombo status={item.value} iconClassName="animate-none" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={9}>
|
||||
<TooltipContent side="right" sideOffset={50}>
|
||||
<Paragraph variant="extra-small">
|
||||
{descriptionForTaskRunStatus(item.value)}
|
||||
</Paragraph>
|
||||
@@ -804,7 +804,7 @@ function RunIdDropdown({
|
||||
disabled={error !== undefined || !runId}
|
||||
variant="secondary/small"
|
||||
shortcut={{
|
||||
modifiers: ["meta"],
|
||||
modifiers: ["mod"],
|
||||
key: "Enter",
|
||||
enabledOnInputElements: true,
|
||||
}}
|
||||
@@ -922,7 +922,7 @@ function BatchIdDropdown({
|
||||
disabled={error !== undefined || !batchId}
|
||||
variant="secondary/small"
|
||||
shortcut={{
|
||||
modifiers: ["meta"],
|
||||
modifiers: ["mod"],
|
||||
key: "Enter",
|
||||
enabledOnInputElements: true,
|
||||
}}
|
||||
@@ -1040,7 +1040,7 @@ function ScheduleIdDropdown({
|
||||
disabled={error !== undefined || !scheduleId}
|
||||
variant="secondary/small"
|
||||
shortcut={{
|
||||
modifiers: ["meta"],
|
||||
modifiers: ["mod"],
|
||||
key: "Enter",
|
||||
enabledOnInputElements: true,
|
||||
}}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function RunInspector({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="h-fit overflow-x-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<TabContainer>
|
||||
<TabButton
|
||||
isActive={!tab || tab === "overview"}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function RunTag({ tag }: { tag: string }) {
|
||||
<span className="flex items-center border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed">
|
||||
{tagResult.key}
|
||||
</span>
|
||||
<span className="flex items-center rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-750 px-1.5 text-text-dimmed">
|
||||
<span className="flex items-center whitespace-nowrap rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-750 px-1.5 text-text-dimmed">
|
||||
{tagResult.value}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-row">
|
||||
<div className="flex w-full">
|
||||
<Input
|
||||
name="search"
|
||||
placeholder="Search schedule id, external id, deduplication id or CRON pattern"
|
||||
@@ -103,7 +103,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
defaultValue={search}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
<SelectGroup>
|
||||
<SelectGroup className="ml-2">
|
||||
<Select name="type" value={type ?? "ALL"} onValueChange={handleTypeChange}>
|
||||
<SelectTrigger size="minimal" width="full">
|
||||
<SelectValue placeholder={"Select type"} className="ml-2 whitespace-nowrap p-0" />
|
||||
|
||||
@@ -390,7 +390,7 @@ export function CustomDateRangeDropdown({
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
shortcut={{
|
||||
modifiers: ["meta"],
|
||||
modifiers: ["mod"],
|
||||
key: "Enter",
|
||||
enabledOnInputElements: true,
|
||||
}}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function SpanInspector({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="h-fit overflow-x-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<TabContainer>
|
||||
<TabButton
|
||||
isActive={!tab || tab === "overview"}
|
||||
|
||||
@@ -2,15 +2,14 @@ import {
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
NoSymbolIcon,
|
||||
PauseCircleIcon,
|
||||
RectangleStackIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type { TaskRunAttemptStatus as TaskRunAttemptStatusType } from "@trigger.dev/database";
|
||||
import { TaskRunAttemptStatus } from "~/database-types";
|
||||
import assertNever from "assert-never";
|
||||
import { SnowflakeIcon } from "lucide-react";
|
||||
import { HourglassIcon, SnowflakeIcon } from "lucide-react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { TaskRunAttemptStatus } from "~/database-types";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export const allTaskRunAttemptStatuses = Object.values(
|
||||
@@ -65,7 +64,7 @@ export function TaskRunAttemptStatusIcon({
|
||||
case "EXECUTING":
|
||||
return <Spinner className={cn(runAttemptStatusClassNameColor(status), className)} />;
|
||||
case "PAUSED":
|
||||
return <SnowflakeIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
|
||||
return <HourglassIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
|
||||
case "FAILED":
|
||||
return <XCircleIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
|
||||
case "CANCELED":
|
||||
@@ -91,7 +90,7 @@ export function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus
|
||||
case "EXECUTING":
|
||||
return "text-pending";
|
||||
case "PAUSED":
|
||||
return "text-sky-300";
|
||||
return "text-charcoal-500";
|
||||
case "FAILED":
|
||||
return "text-error";
|
||||
case "CANCELED":
|
||||
@@ -117,7 +116,7 @@ export function runAttemptStatusTitle(status: ExtendedTaskAttemptStatus | null):
|
||||
case "EXECUTING":
|
||||
return "Executing";
|
||||
case "PAUSED":
|
||||
return "Frozen";
|
||||
return "Waiting";
|
||||
case "FAILED":
|
||||
return "Failed";
|
||||
case "CANCELED":
|
||||
|
||||
@@ -8,13 +8,12 @@ import {
|
||||
NoSymbolIcon,
|
||||
PauseCircleIcon,
|
||||
RectangleStackIcon,
|
||||
StopIcon,
|
||||
TrashIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { TaskRunStatus } from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import { SnowflakeIcon } from "lucide-react";
|
||||
import { HourglassIcon } from "lucide-react";
|
||||
import { TimedOutIcon } from "~/assets/icons/TimedOutIcon";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { cn } from "~/utils/cn";
|
||||
@@ -41,9 +40,9 @@ export const filterableTaskRunStatuses = [
|
||||
"WAITING_FOR_DEPLOY",
|
||||
"DELAYED",
|
||||
"PENDING",
|
||||
"WAITING_TO_RESUME",
|
||||
"EXECUTING",
|
||||
"RETRYING_AFTER_FAILURE",
|
||||
"WAITING_TO_RESUME",
|
||||
"COMPLETED_SUCCESSFULLY",
|
||||
"CANCELED",
|
||||
"COMPLETED_WITH_ERRORS",
|
||||
@@ -55,21 +54,21 @@ export const filterableTaskRunStatuses = [
|
||||
] as const satisfies Readonly<Array<TaskRunStatus>>;
|
||||
|
||||
const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
|
||||
DELAYED: "Task has been delayed and is waiting to be executed",
|
||||
PENDING: "Task is waiting to be executed",
|
||||
WAITING_FOR_DEPLOY: "Task needs to be deployed first to start executing",
|
||||
EXECUTING: "Task is currently being executed",
|
||||
RETRYING_AFTER_FAILURE: "Task is being reattempted after a failure",
|
||||
WAITING_TO_RESUME: "Task has been frozen and is waiting to be resumed",
|
||||
COMPLETED_SUCCESSFULLY: "Task has been successfully completed",
|
||||
CANCELED: "Task has been canceled",
|
||||
COMPLETED_WITH_ERRORS: "Task has failed with errors",
|
||||
INTERRUPTED: "Task has failed because it was interrupted",
|
||||
SYSTEM_FAILURE: "Task has failed due to a system failure",
|
||||
PAUSED: "Task has been paused by the user",
|
||||
CRASHED: "Task has crashed and won't be retried",
|
||||
EXPIRED: "Task has surpassed its ttl and won't be executed",
|
||||
TIMED_OUT: "Task has failed because it exceeded its maxDuration",
|
||||
DELAYED: "Task has been delayed and is waiting to be executed.",
|
||||
PENDING: "Task is waiting to be executed.",
|
||||
WAITING_FOR_DEPLOY: "Task needs to be deployed first to start executing.",
|
||||
EXECUTING: "Task is currently being executed.",
|
||||
RETRYING_AFTER_FAILURE: "Task is being reattempted after a failure.",
|
||||
WAITING_TO_RESUME: `You have used a "wait" function. When the wait is complete, the task will resume execution.`,
|
||||
COMPLETED_SUCCESSFULLY: "Task has been successfully completed.",
|
||||
CANCELED: "Task has been canceled.",
|
||||
COMPLETED_WITH_ERRORS: "Task has failed with errors.",
|
||||
INTERRUPTED: "Task has failed because it was interrupted.",
|
||||
SYSTEM_FAILURE: "Task has failed due to a system failure.",
|
||||
PAUSED: "Task has been paused by the user.",
|
||||
CRASHED: "Task has crashed and won't be retried.",
|
||||
EXPIRED: "Task has surpassed its ttl and won't be executed.",
|
||||
TIMED_OUT: "Task has failed because it exceeded its maxDuration.",
|
||||
};
|
||||
|
||||
export const QUEUED_STATUSES = [
|
||||
@@ -126,7 +125,7 @@ export function TaskRunStatusIcon({
|
||||
case "EXECUTING":
|
||||
return <Spinner className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "WAITING_TO_RESUME":
|
||||
return <SnowflakeIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
return <HourglassIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "RETRYING_AFTER_FAILURE":
|
||||
return <ArrowPathIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
case "PAUSED":
|
||||
@@ -165,7 +164,7 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
|
||||
case "RETRYING_AFTER_FAILURE":
|
||||
return "text-pending";
|
||||
case "WAITING_TO_RESUME":
|
||||
return "text-sky-300";
|
||||
return "text-charcoal-500";
|
||||
case "PAUSED":
|
||||
return "text-amber-300";
|
||||
case "CANCELED":
|
||||
@@ -200,7 +199,7 @@ export function runStatusTitle(status: TaskRunStatus): string {
|
||||
case "EXECUTING":
|
||||
return "Executing";
|
||||
case "WAITING_TO_RESUME":
|
||||
return "Frozen";
|
||||
return "Waiting";
|
||||
case "RETRYING_AFTER_FAILURE":
|
||||
return "Reattempting";
|
||||
case "PAUSED":
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TableVariant,
|
||||
} from "../../primitives/Table";
|
||||
import { CancelRunDialog } from "./CancelRunDialog";
|
||||
import { LiveTimer } from "./LiveTimer";
|
||||
@@ -58,6 +59,7 @@ type RunsTableProps = {
|
||||
runs: RunListItem[];
|
||||
isLoading?: boolean;
|
||||
allowSelection?: boolean;
|
||||
variant?: TableVariant;
|
||||
};
|
||||
|
||||
export function TaskRunsTable({
|
||||
@@ -67,6 +69,7 @@ export function TaskRunsTable({
|
||||
runs,
|
||||
isLoading = false,
|
||||
allowSelection = false,
|
||||
variant = "dimmed",
|
||||
}: RunsTableProps) {
|
||||
const user = useUser();
|
||||
const organization = useOrganization();
|
||||
@@ -104,7 +107,7 @@ export function TaskRunsTable({
|
||||
);
|
||||
|
||||
return (
|
||||
<Table className="max-h-full overflow-y-auto">
|
||||
<Table variant={variant} className="max-h-full overflow-y-auto">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{allowSelection && (
|
||||
@@ -300,7 +303,7 @@ export function TaskRunsTable({
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell to={path} alignment="right">
|
||||
<TableCell to={path} alignment="right" isTabbableCell>
|
||||
{formatNumber(run.number)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
|
||||
@@ -44,9 +44,16 @@ const EnvironmentSchema = z.object({
|
||||
HIGHLIGHT_PROJECT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(),
|
||||
FROM_EMAIL: z.string().optional(),
|
||||
REPLY_TO_EMAIL: z.string().optional(),
|
||||
RESEND_API_KEY: z.string().optional(),
|
||||
SMTP_HOST: z.string().optional(),
|
||||
SMTP_PORT: z.coerce.number().optional(),
|
||||
SMTP_SECURE: z.coerce.boolean().optional(),
|
||||
SMTP_USER: z.string().optional(),
|
||||
SMTP_PASSWORD: z.string().optional(),
|
||||
|
||||
PLAIN_API_KEY: z.string().optional(),
|
||||
RUNTIME_PLATFORM: z.enum(["docker-compose", "ecs", "local"]).default("local"),
|
||||
WORKER_SCHEMA: z.string().default("graphile_worker"),
|
||||
@@ -140,7 +147,10 @@ const EnvironmentSchema = z.object({
|
||||
CONTAINER_REGISTRY_ORIGIN: z.string().optional(),
|
||||
CONTAINER_REGISTRY_USERNAME: z.string().optional(),
|
||||
CONTAINER_REGISTRY_PASSWORD: z.string().optional(),
|
||||
ENABLE_REGISTRY_PROXY: z.string().optional(),
|
||||
DEPLOY_REGISTRY_HOST: z.string().optional(),
|
||||
DEPLOY_REGISTRY_USERNAME: z.string().optional(),
|
||||
DEPLOY_REGISTRY_PASSWORD: z.string().optional(),
|
||||
DEPLOY_REGISTRY_NAMESPACE: z.string().default("trigger"),
|
||||
DEPLOY_TIMEOUT_MS: z.coerce
|
||||
.number()
|
||||
@@ -155,6 +165,8 @@ const EnvironmentSchema = z.object({
|
||||
SHARED_QUEUE_CONSUMER_POOL_SIZE: z.coerce.number().int().default(10),
|
||||
SHARED_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(100),
|
||||
SHARED_QUEUE_CONSUMER_NEXT_TICK_INTERVAL_MS: z.coerce.number().int().default(100),
|
||||
SHARED_QUEUE_CONSUMER_EMIT_RESUME_DEPENDENCY_TIMEOUT_MS: z.coerce.number().int().default(1000),
|
||||
SHARED_QUEUE_CONSUMER_RESOLVE_PAYLOADS_BATCH_SIZE: z.coerce.number().int().default(25),
|
||||
|
||||
// Development OTEL environment variables
|
||||
DEV_OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(),
|
||||
@@ -183,20 +195,28 @@ const EnvironmentSchema = z.object({
|
||||
|
||||
// Internal OTEL environment variables
|
||||
INTERNAL_OTEL_TRACE_EXPORTER_URL: z.string().optional(),
|
||||
INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADER_NAME: z.string().optional(),
|
||||
INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADER_VALUE: z.string().optional(),
|
||||
INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADERS: z.string().optional(),
|
||||
INTERNAL_OTEL_TRACE_LOGGING_ENABLED: z.string().default("1"),
|
||||
// this means 1/20 traces or 5% of traces will be sampled (sampled = recorded)
|
||||
INTERNAL_OTEL_TRACE_SAMPLING_RATE: z.string().default("20"),
|
||||
INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED: z.string().default("0"),
|
||||
INTERNAL_OTEL_TRACE_DISABLED: z.string().default("0"),
|
||||
|
||||
INTERNAL_OTEL_LOG_EXPORTER_URL: z.string().optional(),
|
||||
|
||||
ORG_SLACK_INTEGRATION_CLIENT_ID: z.string().optional(),
|
||||
ORG_SLACK_INTEGRATION_CLIENT_SECRET: z.string().optional(),
|
||||
|
||||
/** These enable the alerts feature in v3 */
|
||||
ALERT_EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(),
|
||||
ALERT_FROM_EMAIL: z.string().optional(),
|
||||
ALERT_REPLY_TO_EMAIL: z.string().optional(),
|
||||
ALERT_RESEND_API_KEY: z.string().optional(),
|
||||
ALERT_SMTP_HOST: z.string().optional(),
|
||||
ALERT_SMTP_PORT: z.coerce.number().optional(),
|
||||
ALERT_SMTP_SECURE: z.coerce.boolean().optional(),
|
||||
ALERT_SMTP_USER: z.string().optional(),
|
||||
ALERT_SMTP_PASSWORD: z.string().optional(),
|
||||
|
||||
MAX_SEQUENTIAL_INDEX_FAILURE_COUNT: z.coerce.number().default(96),
|
||||
|
||||
@@ -206,6 +226,13 @@ const EnvironmentSchema = z.object({
|
||||
.number()
|
||||
.int()
|
||||
.default(60 * 1000 * 15),
|
||||
MARQS_SHARED_QUEUE_LIMIT: z.coerce.number().int().default(1000),
|
||||
MARQS_DEV_QUEUE_LIMIT: z.coerce.number().int().default(1000),
|
||||
MARQS_MAXIMUM_NACK_COUNT: z.coerce.number().int().default(64),
|
||||
MARQS_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75),
|
||||
MARQS_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3),
|
||||
MARQS_QUEUE_AGE_RANDOMIZATION_BIAS: z.coerce.number().default(0.25),
|
||||
|
||||
PROD_TASK_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(),
|
||||
|
||||
VERBOSE_GRAPHILE_LOGGING: z.string().default("false"),
|
||||
@@ -238,13 +265,16 @@ const EnvironmentSchema = z.object({
|
||||
TASK_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().default(524_288), // 512KB
|
||||
TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // 3MB
|
||||
BATCH_TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(1_000_000), // 1MB
|
||||
TASK_RUN_METADATA_MAXIMUM_SIZE: z.coerce.number().int().default(4_096), // 4KB
|
||||
TASK_RUN_METADATA_MAXIMUM_SIZE: z.coerce.number().int().default(262_144), // 256KB
|
||||
|
||||
MAXIMUM_DEV_QUEUE_SIZE: z.coerce.number().int().optional(),
|
||||
MAXIMUM_DEPLOYED_QUEUE_SIZE: z.coerce.number().int().optional(),
|
||||
MAX_BATCH_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500),
|
||||
|
||||
REALTIME_STREAM_VERSION: z.enum(["v1", "v2"]).default("v1"),
|
||||
BATCH_METADATA_OPERATIONS_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000),
|
||||
BATCH_METADATA_OPERATIONS_FLUSH_ENABLED: z.string().default("1"),
|
||||
BATCH_METADATA_OPERATIONS_FLUSH_LOGGING_ENABLED: z.string().default("1"),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getUsername } from "~/utils/username";
|
||||
export type { RuntimeEnvironment };
|
||||
|
||||
export async function findEnvironmentByApiKey(apiKey: string) {
|
||||
const environment = await prisma.runtimeEnvironment.findUnique({
|
||||
const environment = await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
apiKey,
|
||||
},
|
||||
@@ -25,7 +25,7 @@ export async function findEnvironmentByApiKey(apiKey: string) {
|
||||
}
|
||||
|
||||
export async function findEnvironmentByPublicApiKey(apiKey: string) {
|
||||
const environment = await prisma.runtimeEnvironment.findUnique({
|
||||
const environment = await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
pkApiKey: apiKey,
|
||||
},
|
||||
@@ -45,7 +45,7 @@ export async function findEnvironmentByPublicApiKey(apiKey: string) {
|
||||
}
|
||||
|
||||
export async function findEnvironmentById(id: string) {
|
||||
const environment = await prisma.runtimeEnvironment.findUnique({
|
||||
const environment = await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
@@ -85,7 +85,7 @@ export async function createNewSession(environment: RuntimeEnvironment, ipAddres
|
||||
}
|
||||
|
||||
export async function disconnectSession(environmentId: string) {
|
||||
const environment = await prisma.runtimeEnvironment.findUnique({
|
||||
const environment = await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
id: environmentId,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { QueueOptions } from "@trigger.dev/core/v3/schemas";
|
||||
import { TaskQueue } from "@trigger.dev/database";
|
||||
import { prisma } from "~/db.server";
|
||||
|
||||
export async function findQueueInEnvironment(
|
||||
queueName: string,
|
||||
environmentId: string,
|
||||
backgroundWorkerTaskId?: string,
|
||||
backgroundTask?: { queueConfig?: unknown }
|
||||
): Promise<TaskQueue | undefined> {
|
||||
const sanitizedQueueName = sanitizeQueueName(queueName);
|
||||
|
||||
const queue = await prisma.taskQueue.findFirst({
|
||||
where: {
|
||||
runtimeEnvironmentId: environmentId,
|
||||
name: sanitizedQueueName,
|
||||
},
|
||||
});
|
||||
|
||||
if (queue) {
|
||||
return queue;
|
||||
}
|
||||
|
||||
const task = backgroundTask
|
||||
? backgroundTask
|
||||
: backgroundWorkerTaskId
|
||||
? await prisma.backgroundWorkerTask.findFirst({
|
||||
where: {
|
||||
id: backgroundWorkerTaskId,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queueConfig = QueueOptions.safeParse(task.queueConfig);
|
||||
|
||||
if (queueConfig.success) {
|
||||
const taskQueueName = queueConfig.data.name
|
||||
? sanitizeQueueName(queueConfig.data.name)
|
||||
: undefined;
|
||||
|
||||
if (taskQueueName && taskQueueName !== sanitizedQueueName) {
|
||||
const queue = await prisma.taskQueue.findFirst({
|
||||
where: {
|
||||
runtimeEnvironmentId: environmentId,
|
||||
name: taskQueueName,
|
||||
},
|
||||
});
|
||||
|
||||
if (queue) {
|
||||
return queue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow alphanumeric characters, underscores, hyphens, and slashes (and only the first 128 characters)
|
||||
export function sanitizeQueueName(queueName: string) {
|
||||
return queueName.replace(/[^a-zA-Z0-9_\-\/]/g, "").substring(0, 128);
|
||||
}
|
||||
@@ -224,10 +224,15 @@ export class ApiRunListPresenter extends BasePresenter {
|
||||
|
||||
const data: ListRunResponseItem[] = await Promise.all(
|
||||
results.runs.map(async (run) => {
|
||||
const metadata = await parsePacket({
|
||||
data: run.metadata ?? undefined,
|
||||
dataType: run.metadataType,
|
||||
});
|
||||
const metadata = await parsePacket(
|
||||
{
|
||||
data: run.metadata ?? undefined,
|
||||
dataType: run.metadataType,
|
||||
},
|
||||
{
|
||||
filteredKeys: ["$$streams", "$$streamsVersion", "$$streamsBaseUrl"],
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: run.friendlyId,
|
||||
|
||||
@@ -216,7 +216,7 @@ export class SpanPresenter extends BasePresenter {
|
||||
|
||||
const metadata = run.metadata
|
||||
? await prettyPrintPacket(run.metadata, run.metadataType, {
|
||||
filteredKeys: ["$$streams", "$$streamsVersion"],
|
||||
filteredKeys: ["$$streams", "$$streamsVersion", "$$streamsBaseUrl"],
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { BackgroundWorkerTask, RuntimeEnvironmentType, TaskRunStatus } from "@tr
|
||||
import { PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server";
|
||||
import {
|
||||
BackgroundWorkerTaskSlim,
|
||||
findCurrentWorkerDeployment,
|
||||
} from "~/v3/models/workerDeployment.server";
|
||||
|
||||
type TestTaskOptions = {
|
||||
userId: string;
|
||||
@@ -113,7 +116,7 @@ export class TestTaskPresenter {
|
||||
},
|
||||
});
|
||||
|
||||
let task: BackgroundWorkerTask | null = null;
|
||||
let task: BackgroundWorkerTaskSlim | null = null;
|
||||
if (environment.type !== "DEVELOPMENT") {
|
||||
const deployment = await findCurrentWorkerDeployment(environment.id);
|
||||
if (deployment) {
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import simplur from "simplur";
|
||||
import { Link, MetaFunction } from "@remix-run/react";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import {
|
||||
PageAccessories,
|
||||
NavBar,
|
||||
PageInfoGroup,
|
||||
PageInfoRow,
|
||||
PageTitle,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { newProjectPath, projectPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: "Projects | Trigger.dev",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
|
||||
|
||||
+75
-58
@@ -8,7 +8,7 @@ import {
|
||||
UserPlusIcon,
|
||||
VideoCameraIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { json } from "@remix-run/node";
|
||||
import { json, type MetaFunction } from "@remix-run/node";
|
||||
import { Link, useRevalidator, useSubmit } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
@@ -20,7 +20,12 @@ import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommandV3, TriggerDevStepV3, TriggerLoginStepV3 } from "~/components/SetupCommands";
|
||||
import {
|
||||
InitCommandV3,
|
||||
PackageManagerProvider,
|
||||
TriggerDevStepV3,
|
||||
TriggerLoginStepV3,
|
||||
} from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
@@ -84,6 +89,14 @@ import {
|
||||
v3TestTaskPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Tasks | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
|
||||
@@ -224,7 +237,7 @@ export default function Page() {
|
||||
<div className="flex min-w-0 max-w-full flex-col">
|
||||
{!userHasTasks && <UserHasNoTasks />}
|
||||
<div className="max-h-full overflow-hidden">
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex items-center gap-1 p-2">
|
||||
<Input
|
||||
placeholder="Search tasks"
|
||||
variant="tertiary"
|
||||
@@ -282,7 +295,7 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<TableRow key={task.slug} className="group">
|
||||
<TableCell to={path}>
|
||||
<TableCell to={path} isTabbableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<SimpleTooltip
|
||||
button={<TaskTriggerSourceIcon source={task.triggerSource} />}
|
||||
@@ -291,13 +304,13 @@ export default function Page() {
|
||||
<span>{task.slug}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="py-0" actionClassName="py-0">
|
||||
<TableCell to={path}>
|
||||
<TaskFunctionName
|
||||
functionName={task.exportName}
|
||||
variant="extra-extra-small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<TableCell to={path}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
@@ -313,7 +326,7 @@ export default function Page() {
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<TableCell to={path}>
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={runningStats}>
|
||||
{(data) => {
|
||||
@@ -323,7 +336,7 @@ export default function Page() {
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0" actionClassName="py-0">
|
||||
<TableCell to={path} actionClassName="py-1.5">
|
||||
<Suspense fallback={<TaskActivityBlankState />}>
|
||||
<TypedAwait resolve={activity}>
|
||||
{(data) => {
|
||||
@@ -343,7 +356,7 @@ export default function Page() {
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<TableCell to={path}>
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={durations}>
|
||||
{(data) => {
|
||||
@@ -431,38 +444,40 @@ export default function Page() {
|
||||
|
||||
function CreateTaskInstructions() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between border-b">
|
||||
<Header1 spacing>Get setup in 3 minutes</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="minimal/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
<PackageManagerProvider>
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between border-b">
|
||||
<Header1 spacing>Get setup in 3 minutes</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="minimal/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<StepNumber stepNumber="1" title="Run the CLI 'init' command in an existing project" />
|
||||
<StepContentContainer>
|
||||
<InitCommandV3 />
|
||||
<Paragraph spacing>
|
||||
You'll notice a new folder in your project called{" "}
|
||||
<InlineCode variant="small">trigger</InlineCode>. We've added a very simple example task
|
||||
in here to help you get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Waiting for tasks" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
<StepNumber stepNumber="1" title="Run the CLI 'init' command in your project" />
|
||||
<StepContentContainer>
|
||||
<InitCommandV3 />
|
||||
<Paragraph spacing>
|
||||
You’ll notice a new folder in your project called{" "}
|
||||
<InlineCode variant="small">trigger</InlineCode>. We’ve added a very simple example task
|
||||
in here to help you get started.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Waiting for tasks" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</PackageManagerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -484,26 +499,28 @@ function UserHasNoTasks() {
|
||||
}
|
||||
>
|
||||
{open ? (
|
||||
<div>
|
||||
<Header2 spacing>Get setup in 3 minutes</Header2>
|
||||
<PackageManagerProvider>
|
||||
<div>
|
||||
<Header2 spacing>Get setup in 3 minutes</Header2>
|
||||
|
||||
<StepNumber stepNumber="1" title="Open up your project" className="mt-6" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>You'll need to open a terminal at the root of your project.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run the CLI 'login' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerLoginStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Waiting for tasks" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
<StepNumber stepNumber="1" title="Open up your project" className="mt-6" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>You'll need to open a terminal at the root of your project.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run the CLI 'login' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerLoginStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Waiting for tasks" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</PackageManagerProvider>
|
||||
) : (
|
||||
"Your DEV environment isn't setup yet."
|
||||
)}
|
||||
|
||||
+11
-4
@@ -7,11 +7,10 @@ import {
|
||||
EnvelopeIcon,
|
||||
GlobeAltIcon,
|
||||
LockClosedIcon,
|
||||
LockOpenIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Form, Outlet, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { Form, MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { type ProjectAlertChannelType, type ProjectAlertType } from "@trigger.dev/database";
|
||||
@@ -24,7 +23,6 @@ import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DetailCell } from "~/components/primitives/DetailCell";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
@@ -63,6 +61,14 @@ import {
|
||||
v3ProjectAlertsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Alerts | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam, organizationSlug } = ProjectParamSchema.parse(params);
|
||||
@@ -216,7 +222,7 @@ export default function Page() {
|
||||
/>
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell className={alertChannel.enabled ? "" : "opacity-50"}>
|
||||
<TableCell className={cn("py-1", alertChannel.enabled ? "" : "opacity-50")}>
|
||||
<AlertChannelDetails alertChannel={alertChannel} />
|
||||
</TableCell>
|
||||
<TableCell className={alertChannel.enabled ? "" : "opacity-50"}>
|
||||
@@ -460,6 +466,7 @@ function AlertChannelDetails({ alertChannel }: { alertChannel: AlertChannelListP
|
||||
label={"Email"}
|
||||
description={alertChannel.properties.email}
|
||||
boxClassName="group-hover/table-row:bg-charcoal-800"
|
||||
className="h-12"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
import { BookOpenIcon, InformationCircleIcon, LockOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { MetaFunction } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
@@ -28,6 +29,14 @@ import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { ProjectParamSchema, docsPath, v3BillingPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `API keys | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
+13
-3
@@ -4,7 +4,7 @@ import {
|
||||
ExclamationCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { useLocation, useNavigation } from "@remix-run/react";
|
||||
import { MetaFunction, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -50,6 +50,14 @@ import {
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, ProjectParamSchema, v3BatchRunsPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Batches | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam, organizationSlug } = ProjectParamSchema.parse(params);
|
||||
@@ -187,7 +195,9 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
|
||||
const path = v3BatchRunsPath(organization, project, batch);
|
||||
return (
|
||||
<TableRow key={batch.id}>
|
||||
<TableCell to={path}>{batch.friendlyId}</TableCell>
|
||||
<TableCell to={path} isTabbableCell>
|
||||
{batch.friendlyId}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnvironmentLabel
|
||||
environment={batch.environment}
|
||||
@@ -201,7 +211,7 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) {
|
||||
disableHoverableContent
|
||||
button={
|
||||
<span className="flex items-center gap-1">
|
||||
<ExclamationCircleIcon className="size-4 text-slate-500" />
|
||||
<ExclamationCircleIcon className="size-4 text-text-dimmed" />
|
||||
<span>Legacy batch</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
+11
-4
@@ -3,7 +3,8 @@ import {
|
||||
BookOpenIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Await } from "@remix-run/react";
|
||||
import { LockOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { Await, MetaFunction } from "@remix-run/react";
|
||||
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Suspense } from "react";
|
||||
import { typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -12,8 +13,8 @@ import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import {
|
||||
Table,
|
||||
@@ -31,8 +32,14 @@ import {
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, ProjectParamSchema, v3BillingPath } from "~/utils/pathBuilder";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { LockOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Concurrency limits | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
+106
-96
@@ -4,7 +4,6 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { UserAvatar } from "~/components/UserProfilePhoto";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
@@ -108,107 +107,118 @@ export default function Page() {
|
||||
|
||||
<LinkButton
|
||||
to={`${v3DeploymentsPath(organization, project)}${page ? `?page=${page}` : ""}`}
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={ExitIcon}
|
||||
variant="minimal/small"
|
||||
TrailingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
shortcutPosition="before-trailing-icon"
|
||||
className="pl-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-auto px-3 pt-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>Deploy</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<span>{deployment.shortCode}</span>
|
||||
{deployment.label && <Badge variant="outline-rounded">{deployment.label}</Badge>}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Environment</Property.Label>
|
||||
<Property.Value>
|
||||
<EnvironmentLabel environment={deployment.environment} userName={usernameForEnv} />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Version</Property.Label>
|
||||
<Property.Value>{deployment.version}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Status</Property.Label>
|
||||
<Property.Value>
|
||||
<DeploymentStatus
|
||||
status={deployment.status}
|
||||
isBuilt={deployment.isBuilt}
|
||||
className="text-sm"
|
||||
/>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Tasks</Property.Label>
|
||||
<Property.Value>{deployment.tasks ? deployment.tasks.length : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>SDK Version</Property.Label>
|
||||
<Property.Value>{deployment.sdkVersion ? deployment.sdkVersion : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>CLI Version</Property.Label>
|
||||
<Property.Value>{deployment.cliVersion ? deployment.cliVersion : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Started at</Property.Label>
|
||||
<Property.Value>
|
||||
<DateTimeAccurate date={deployment.createdAt} /> UTC
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Built at</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.builtAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.builtAt} /> UTC
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Deployed at</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.deployedAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.deployedAt} /> UTC
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Deployed by</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.deployedBy ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<UserAvatar
|
||||
avatarUrl={deployment.deployedBy.avatarUrl}
|
||||
name={deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Paragraph variant="small">
|
||||
{deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
</Property.Table>
|
||||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="flex flex-col">
|
||||
<div className="p-3">
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>Deploy</Property.Label>
|
||||
<Property.Value className="flex items-center gap-2">
|
||||
<span>{deployment.shortCode}</span>
|
||||
{deployment.label && <Badge variant="outline-rounded">{deployment.label}</Badge>}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Environment</Property.Label>
|
||||
<Property.Value>
|
||||
<EnvironmentLabel
|
||||
environment={deployment.environment}
|
||||
userName={usernameForEnv}
|
||||
/>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Version</Property.Label>
|
||||
<Property.Value>{deployment.version}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Status</Property.Label>
|
||||
<Property.Value>
|
||||
<DeploymentStatus
|
||||
status={deployment.status}
|
||||
isBuilt={deployment.isBuilt}
|
||||
className="text-sm"
|
||||
/>
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Tasks</Property.Label>
|
||||
<Property.Value>{deployment.tasks ? deployment.tasks.length : "–"}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>SDK Version</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.sdkVersion ? deployment.sdkVersion : "–"}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>CLI Version</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.cliVersion ? deployment.cliVersion : "–"}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Started at</Property.Label>
|
||||
<Property.Value>
|
||||
<DateTimeAccurate date={deployment.createdAt} /> UTC
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Built at</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.builtAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.builtAt} /> UTC
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Deployed at</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.deployedAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.deployedAt} /> UTC
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Deployed by</Property.Label>
|
||||
<Property.Value>
|
||||
{deployment.deployedBy ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<UserAvatar
|
||||
avatarUrl={deployment.deployedBy.avatarUrl}
|
||||
name={deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Paragraph variant="small">
|
||||
{deployment.deployedBy.name ?? deployment.deployedBy.displayName}
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
</Property.Table>
|
||||
</div>
|
||||
|
||||
{deployment.tasks ? (
|
||||
<div className="divide-y divide-charcoal-800 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<Table>
|
||||
<Table variant="bright">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="px-2">Task</TableHeaderCell>
|
||||
|
||||
+61
-17
@@ -4,7 +4,7 @@ import {
|
||||
BookOpenIcon,
|
||||
ServerStackIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
@@ -35,7 +35,11 @@ import {
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus";
|
||||
import {
|
||||
DeploymentStatus,
|
||||
deploymentStatusDescription,
|
||||
deploymentStatuses,
|
||||
} from "~/components/runs/v3/DeploymentStatus";
|
||||
import { RetryDeploymentIndexingDialog } from "~/components/runs/v3/RetryDeploymentIndexingDialog";
|
||||
import { RollbackDeploymentDialog } from "~/components/runs/v3/RollbackDeploymentDialog";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
@@ -46,7 +50,6 @@ import {
|
||||
DeploymentListPresenter,
|
||||
} from "~/presenters/v3/DeploymentListPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
ProjectParamSchema,
|
||||
docsPath,
|
||||
@@ -56,6 +59,14 @@ import {
|
||||
import { createSearchParams } from "~/utils/searchParams";
|
||||
import { deploymentIndexingIsRetryable } from "~/v3/deploymentStatus";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Deployments | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const SearchParams = z.object({
|
||||
page: z.coerce.number().optional(),
|
||||
});
|
||||
@@ -120,7 +131,30 @@ export default function Page() {
|
||||
<TableHeaderCell>Deploy</TableHeaderCell>
|
||||
<TableHeaderCell>Env</TableHeaderCell>
|
||||
<TableHeaderCell>Version</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell
|
||||
tooltip={
|
||||
<div className="flex flex-col divide-y divide-grid-dimmed">
|
||||
{deploymentStatuses.map((status) => (
|
||||
<div
|
||||
key={status}
|
||||
className="grid grid-cols-[8rem_1fr] gap-x-2 py-2 first:pt-1 last:pb-1"
|
||||
>
|
||||
<div className="mb-0.5 flex items-center gap-1.5 whitespace-nowrap">
|
||||
<DeploymentStatus status={status} isBuilt={false} />
|
||||
</div>
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="!text-wrap text-text-dimmed"
|
||||
>
|
||||
{deploymentStatusDescription(status)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Status
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>Tasks</TableHeaderCell>
|
||||
<TableHeaderCell>Deployed at</TableHeaderCell>
|
||||
<TableHeaderCell>Deployed by</TableHeaderCell>
|
||||
@@ -142,11 +176,8 @@ export default function Page() {
|
||||
);
|
||||
const isSelected = deploymentParam === deployment.shortCode;
|
||||
return (
|
||||
<TableRow
|
||||
key={deployment.id}
|
||||
className={cn("group", isSelected ? "bg-grid-dimmed" : undefined)}
|
||||
>
|
||||
<TableCell to={path}>
|
||||
<TableRow key={deployment.id} className="group" isSelected={isSelected}>
|
||||
<TableCell to={path} isTabbableCell isSelected={isSelected}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small">{deployment.shortCode}</Paragraph>
|
||||
{deployment.label && (
|
||||
@@ -154,30 +185,32 @@ export default function Page() {
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<TableCell to={path} isSelected={isSelected}>
|
||||
<EnvironmentLabel
|
||||
environment={deployment.environment}
|
||||
userName={usernameForEnv}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path}>{deployment.version}</TableCell>
|
||||
<TableCell to={path}>
|
||||
<TableCell to={path} isSelected={isSelected}>
|
||||
{deployment.version}
|
||||
</TableCell>
|
||||
<TableCell to={path} isSelected={isSelected}>
|
||||
<DeploymentStatus
|
||||
status={deployment.status}
|
||||
isBuilt={deployment.isBuilt}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<TableCell to={path} isSelected={isSelected}>
|
||||
{deployment.tasksCount !== null ? deployment.tasksCount : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<TableCell to={path} isSelected={isSelected}>
|
||||
{deployment.deployedAt ? (
|
||||
<DateTime date={deployment.deployedAt} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<TableCell to={path} isSelected={isSelected}>
|
||||
{deployment.deployedBy ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<UserAvatar
|
||||
@@ -197,7 +230,11 @@ export default function Page() {
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<DeploymentActionsCell deployment={deployment} path={path} />
|
||||
<DeploymentActionsCell
|
||||
deployment={deployment}
|
||||
path={path}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
@@ -282,9 +319,11 @@ function CreateDeploymentInstructions() {
|
||||
function DeploymentActionsCell({
|
||||
deployment,
|
||||
path,
|
||||
isSelected,
|
||||
}: {
|
||||
deployment: DeploymentListItem;
|
||||
path: string;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const project = useProject();
|
||||
@@ -293,12 +332,17 @@ function DeploymentActionsCell({
|
||||
const canRetryIndexing = deployment.isLatest && deploymentIndexingIsRetryable(deployment);
|
||||
|
||||
if (!canRollback && !canRetryIndexing) {
|
||||
return <TableCell to={path}>{""}</TableCell>;
|
||||
return (
|
||||
<TableCell to={path} isSelected={isSelected}>
|
||||
{""}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCellMenu
|
||||
isSticky
|
||||
isSelected={isSelected}
|
||||
popoverContent={
|
||||
<>
|
||||
{canRollback && (
|
||||
|
||||
+9
-1
@@ -9,7 +9,7 @@ import {
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Form, Outlet, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { Form, MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react";
|
||||
import {
|
||||
ActionFunctionArgs,
|
||||
LoaderFunctionArgs,
|
||||
@@ -69,6 +69,14 @@ import {
|
||||
EditEnvironmentVariable,
|
||||
} from "~/v3/environmentVariables/repository";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Environment variables | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
+11
-3
@@ -1,6 +1,6 @@
|
||||
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import { Form, MetaFunction, useNavigation } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { IconCircleX } from "@tabler/icons-react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
@@ -53,6 +53,14 @@ import {
|
||||
} from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../../components/ListPagination";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Runs | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam, organizationSlug } = ProjectParamSchema.parse(params);
|
||||
@@ -346,7 +354,7 @@ function CancelRuns({ onOpen }: { onOpen: (open: boolean) => void }) {
|
||||
variant="danger/medium"
|
||||
LeadingIcon={isLoading ? "spinner-white" : StopCircleIcon}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
shortcut={{ modifiers: ["mod"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Canceling..." : `Cancel ${selectedItems.size} runs`}
|
||||
</Button>
|
||||
@@ -403,7 +411,7 @@ function ReplayRuns({ onOpen }: { onOpen: (open: boolean) => void }) {
|
||||
variant="primary/medium"
|
||||
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
shortcut={{ modifiers: ["mod"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Replaying..." : `Replay ${selectedItems.size} runs`}
|
||||
</Button>
|
||||
|
||||
+58
-54
@@ -220,11 +220,12 @@ export default function Page() {
|
||||
TrailingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
shortcutPosition="before-trailing-icon"
|
||||
className="pl-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="p-3">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3">
|
||||
<div className="p-3">
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>Schedule ID</Property.Label>
|
||||
@@ -282,61 +283,64 @@ export default function Page() {
|
||||
</>
|
||||
)}
|
||||
</Property.Table>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3>Last 5 runs</Header3>
|
||||
<TaskRunsTable
|
||||
total={schedule.runs.length}
|
||||
hasFilters={false}
|
||||
filters={{
|
||||
tasks: [],
|
||||
versions: [],
|
||||
statuses: [],
|
||||
environments: [],
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
}}
|
||||
runs={schedule.runs}
|
||||
isLoading={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3>Next 5 runs</Header3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
|
||||
<TableHeaderCell>UTC</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{schedule.active ? (
|
||||
schedule.nextRuns.length ? (
|
||||
schedule.nextRuns.map((run, index) => (
|
||||
<TableRow key={index}>
|
||||
{!isUtc && (
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone={schedule.timezone} />
|
||||
</TableCell>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3 className="pb-1 pl-3">Last 5 runs</Header3>
|
||||
<TaskRunsTable
|
||||
total={schedule.runs.length}
|
||||
hasFilters={false}
|
||||
filters={{
|
||||
tasks: [],
|
||||
versions: [],
|
||||
statuses: [],
|
||||
environments: [],
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
}}
|
||||
runs={schedule.runs}
|
||||
isLoading={false}
|
||||
variant="bright"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 pt-2">
|
||||
<Header3 className="pb-1 pl-3">Next 5 runs</Header3>
|
||||
<Table variant="bright">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
|
||||
<TableHeaderCell>UTC</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{schedule.active ? (
|
||||
schedule.nextRuns.length ? (
|
||||
schedule.nextRuns.map((run, index) => (
|
||||
<TableRow key={index}>
|
||||
{!isUtc && (
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone="UTC" />
|
||||
<DateTime date={run} timeZone={schedule.timezone} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableBlankRow colSpan={isUtc ? 1 : 2}>
|
||||
<PlaceholderText title="You found a bug" />
|
||||
</TableBlankRow>
|
||||
)
|
||||
)}
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone="UTC" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableBlankRow colSpan={isUtc ? 1 : 2}>
|
||||
<PlaceholderText title="Schedule disabled" />
|
||||
<PlaceholderText title="You found a bug" />
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{!isImperative && (
|
||||
)
|
||||
) : (
|
||||
<TableBlankRow colSpan={isUtc ? 1 : 2}>
|
||||
<PlaceholderText title="Schedule disabled" />
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{!isImperative && (
|
||||
<div className="p-3">
|
||||
<InfoPanel
|
||||
title="Editing declarative schedules"
|
||||
icon={BookOpenIcon}
|
||||
@@ -349,8 +353,8 @@ export default function Page() {
|
||||
You can only edit a declarative schedule by updating your schedules.task and then
|
||||
running the CLI dev and deploy commands.
|
||||
</InfoPanel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isImperative && (
|
||||
|
||||
+23
-4
@@ -1,6 +1,6 @@
|
||||
import { ClockIcon, LockOpenIcon, PlusIcon, RectangleGroupIcon } from "@heroicons/react/20/solid";
|
||||
import { BookOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
@@ -65,6 +65,15 @@ import {
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Schedules | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -210,9 +219,19 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-fit max-h-full overflow-x-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-fit max-h-full min-h-full overflow-x-auto",
|
||||
totalPages > 1 ? "grid-rows-[1fr_auto]" : "grid-rows-[1fr]"
|
||||
)}
|
||||
>
|
||||
<SchedulesTable schedules={schedules} hasFilters={hasFilters} />
|
||||
<div className="flex justify-end py-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-full",
|
||||
totalPages > 1 && "justify-end border-t border-grid-dimmed px-2 py-3"
|
||||
)}
|
||||
>
|
||||
<PaginationControls currentPage={currentPage} totalPages={totalPages} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -443,7 +462,7 @@ function SchedulesTable({
|
||||
const cellClass = schedule.active ? "" : "opacity-50";
|
||||
return (
|
||||
<TableRow key={schedule.id} className={isSelected ? "bg-grid-dimmed" : undefined}>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<TableCell to={path} isTabbableCell className={cellClass}>
|
||||
{schedule.friendlyId}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
|
||||
+9
-1
@@ -1,6 +1,6 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { Form, MetaFunction, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunction, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
@@ -24,6 +24,14 @@ import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { v3ProjectPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Project settings | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export function createSchema(
|
||||
constraints: {
|
||||
getSlugMatch?: (slug: string) => { isMatch: boolean; projectSlug: string };
|
||||
|
||||
+17
-1
@@ -1,5 +1,12 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { Link, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react";
|
||||
import {
|
||||
Link,
|
||||
MetaFunction,
|
||||
Outlet,
|
||||
useLocation,
|
||||
useNavigation,
|
||||
useParams,
|
||||
} from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
@@ -47,6 +54,14 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { docsPath, ProjectParamSchema, v3TestPath, v3TestTaskPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Test | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const TestSearchParams = z.object({
|
||||
environment: z.string().optional(),
|
||||
});
|
||||
@@ -266,6 +281,7 @@ function TaskRow({ task, environmentSlug }: { task: TaskListItem; environmentSlu
|
||||
</TableCell>
|
||||
<TableCell
|
||||
to={path}
|
||||
isTabbableCell
|
||||
actionClassName="pl-1 pr-2 py-1.5"
|
||||
className={cn((isActive || isPending) && "group-hover/table-row:bg-indigo-500/5")}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { Form, MetaFunction, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunction, json } from "@remix-run/server-runtime";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
@@ -28,6 +28,14 @@ import { logger } from "~/services/logger.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { organizationPath, organizationSettingsPath, rootPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Organization settings | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export function createSchema(
|
||||
constraints: {
|
||||
getSlugMatch?: (slug: string) => { isMatch: boolean; organizationSlug: string };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { LockOpenIcon, UserPlusIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { Form, MetaFunction, useActionData } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { useState } from "react";
|
||||
import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -44,6 +44,14 @@ import {
|
||||
v3BillingPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Team | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const Params = z.object({
|
||||
organizationSlug: z.string(),
|
||||
});
|
||||
@@ -209,6 +217,7 @@ export default function Page() {
|
||||
title="Unlock more team members"
|
||||
to={v3BillingPath(organization)}
|
||||
buttonLabel="Upgrade"
|
||||
panelClassName="mt-4 max-w-sm"
|
||||
>
|
||||
<Paragraph variant="small">
|
||||
You've used all {limits.limit} of your available team members. Upgrade your plan to
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { CalendarDaysIcon, StarIcon } from "@heroicons/react/20/solid";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { PlanDefinition } from "@trigger.dev/platform/v3";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { prisma } from "~/db.server";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import {
|
||||
OrganizationParamsSchema,
|
||||
@@ -12,10 +16,15 @@ import {
|
||||
v3StripePortalPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan";
|
||||
import { PlanDefinition } from "@trigger.dev/platform/v3";
|
||||
import { CalendarDaysIcon, StarIcon } from "@heroicons/react/20/solid";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
|
||||
import { MetaFunction } from "@remix-run/react";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Billing | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/24/solid";
|
||||
import { Await } from "@remix-run/react";
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Await, MetaFunction } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { Suspense } from "react";
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "~/components/primitives/Chart";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
@@ -35,8 +36,14 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { formatCurrency, formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
|
||||
import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Usage | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { Form, MetaFunction, useActionData } from "@remix-run/react";
|
||||
import { ActionFunction, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
|
||||
@@ -22,6 +22,14 @@ import { updateUser } from "~/models/user.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { accountPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Your profile | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
function createSchema(
|
||||
constraints: {
|
||||
isEmailUnique?: (email: string) => Promise<boolean>;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { parse } from "@conform-to/zod";
|
||||
import { BookOpenIcon, ShieldCheckIcon, TrashIcon } from "@heroicons/react/20/solid";
|
||||
import { ShieldExclamationIcon } from "@heroicons/react/24/solid";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import { Form, useActionData, useFetcher } from "@remix-run/react";
|
||||
import { Form, MetaFunction, useActionData, useFetcher } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
@@ -45,6 +45,14 @@ import {
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, personalAccessTokensPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{
|
||||
title: `Personal Access Tokens | Trigger.dev`,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
|
||||
import { marqs } from "~/v3/marqs/index.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: authenticationResult.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!user.admin) {
|
||||
return json({ error: "You must be an admin to perform this action" }, { status: 403 });
|
||||
}
|
||||
|
||||
const details = await marqs?.getSharedQueueDetails();
|
||||
|
||||
return json(details);
|
||||
}
|
||||
@@ -1,94 +1,29 @@
|
||||
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { parsePacket, UpdateMetadataRequestBody } from "@trigger.dev/core/v3";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { UpdateMetadataRequestBody } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { handleMetadataPacket } from "~/utils/packets";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { isFinalRunStatus } from "~/v3/taskStatus";
|
||||
import { updateMetadataService } from "~/services/metadata/updateMetadata.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
});
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
// Ensure this is a PUT request
|
||||
if (request.method.toUpperCase() !== "PUT") {
|
||||
return json({ error: "Method not allowed" }, { status: 405, headers: { Allow: "PUT" } });
|
||||
}
|
||||
const { action } = createActionApiRoute(
|
||||
{
|
||||
params: ParamsSchema,
|
||||
body: UpdateMetadataRequestBody,
|
||||
maxContentLength: 1024 * 1024, // 1MB
|
||||
method: "PUT",
|
||||
},
|
||||
async ({ authentication, body, params }) => {
|
||||
const result = await updateMetadataService.call(params.runId, body, authentication.environment);
|
||||
|
||||
// Authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const parsedParams = ParamsSchema.safeParse(params);
|
||||
if (!parsedParams.success) {
|
||||
return json(
|
||||
{ error: "Invalid request parameters", issues: parsedParams.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const anyBody = await request.json();
|
||||
|
||||
const body = UpdateMetadataRequestBody.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const metadataPacket = handleMetadataPacket(
|
||||
body.data.metadata,
|
||||
body.data.metadataType ?? "application/json"
|
||||
);
|
||||
|
||||
if (!metadataPacket) {
|
||||
return json({ error: "Invalid metadata" }, { status: 400 });
|
||||
}
|
||||
|
||||
const taskRun = await prisma.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: parsedParams.data.runId,
|
||||
runtimeEnvironmentId: authenticationResult.environment.id,
|
||||
},
|
||||
select: {
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!taskRun) {
|
||||
if (!result) {
|
||||
return json({ error: "Task Run not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (isFinalRunStatus(taskRun.status)) {
|
||||
return json({ error: "Cannot update metadata for a completed run" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.taskRun.update({
|
||||
where: {
|
||||
friendlyId: parsedParams.data.runId,
|
||||
runtimeEnvironmentId: authenticationResult.environment.id,
|
||||
},
|
||||
data: {
|
||||
metadata: metadataPacket?.data,
|
||||
metadataType: metadataPacket?.dataType,
|
||||
},
|
||||
});
|
||||
|
||||
const parsedPacket = await parsePacket(metadataPacket);
|
||||
|
||||
return json({ metadata: parsedPacket }, { status: 200 });
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json({ error: error.message }, { status: error.status ?? 422 });
|
||||
} else {
|
||||
return json(
|
||||
{ error: error instanceof Error ? error.message : "Internal Server Error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return json(result, { status: 200 });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export { action };
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import {
|
||||
BatchTriggerTaskResponse,
|
||||
BatchTriggerTaskV2RequestBody,
|
||||
BatchTriggerTaskV2Response,
|
||||
generateJWT,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { env } from "~/env.server";
|
||||
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
|
||||
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import {
|
||||
BatchProcessingStrategy,
|
||||
BatchTriggerV2Service,
|
||||
} from "~/v3/services/batchTriggerV2.server";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { OutOfEntitlementError } from "~/v3/services/triggerTask.server";
|
||||
import { AuthenticatedEnvironment, getOneTimeUseToken } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { z } from "zod";
|
||||
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
|
||||
|
||||
const { action, loader } = createActionApiRoute(
|
||||
{
|
||||
|
||||
+24
-13
@@ -1,9 +1,10 @@
|
||||
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { StartDeploymentIndexingRequestBody } from "@trigger.dev/core/v3";
|
||||
import { FinalizeDeploymentRequestBody } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { StartDeploymentIndexing } from "~/v3/services/startDeploymentIndexing.server";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { FinalizeDeploymentV2Service } from "~/v3/services/finalizeDeploymentV2";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
deploymentId: z.string(),
|
||||
@@ -34,21 +35,31 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
const { deploymentId } = parsedParams.data;
|
||||
|
||||
const rawBody = await request.json();
|
||||
const body = StartDeploymentIndexingRequestBody.safeParse(rawBody);
|
||||
const body = FinalizeDeploymentRequestBody.safeParse(rawBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const service = new StartDeploymentIndexing();
|
||||
try {
|
||||
const service = new FinalizeDeploymentV2Service();
|
||||
await service.call(authenticatedEnv, deploymentId, body.data);
|
||||
|
||||
const deployment = await service.call(authenticatedEnv, deploymentId, body.data);
|
||||
|
||||
return json(
|
||||
{
|
||||
id: deployment.friendlyId,
|
||||
contentHash: deployment.contentHash,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
return json(
|
||||
{
|
||||
id: deploymentId,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
} else if (error instanceof Error) {
|
||||
logger.error("Error finalizing deployment", { error: error.message });
|
||||
return json({ error: `Internal server error: ${error.message}` }, { status: 500 });
|
||||
} else {
|
||||
logger.error("Error finalizing deployment", { error: String(error) });
|
||||
return json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { $replica } from "~/db.server";
|
||||
import { v1RealtimeStreams } from "~/services/realtime/v1StreamsGlobal.server";
|
||||
import { relayRealtimeStreams } from "~/services/realtime/relayRealtimeStreams.server";
|
||||
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
@@ -16,7 +16,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
return new Response("No body provided", { status: 400 });
|
||||
}
|
||||
|
||||
return v1RealtimeStreams.ingestData(request.body, $params.runId, $params.streamId);
|
||||
return relayRealtimeStreams.ingestData(request.body, $params.runId, $params.streamId);
|
||||
}
|
||||
|
||||
export const loader = createLoaderApiRoute(
|
||||
@@ -51,7 +51,7 @@ export const loader = createLoaderApiRoute(
|
||||
},
|
||||
},
|
||||
async ({ params, request, resource: run, authentication }) => {
|
||||
return v1RealtimeStreams.streamResponse(
|
||||
return relayRealtimeStreams.streamResponse(
|
||||
request,
|
||||
run.friendlyId,
|
||||
params.streamId,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { z } from "zod";
|
||||
import { $replica } from "~/db.server";
|
||||
import { relayRealtimeStreams } from "~/services/realtime/relayRealtimeStreams.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
target: z.enum(["self", "parent", "root"]),
|
||||
streamId: z.string(),
|
||||
});
|
||||
|
||||
const { action } = createActionApiRoute(
|
||||
{
|
||||
params: ParamsSchema,
|
||||
},
|
||||
async ({ request, params, authentication }) => {
|
||||
if (!request.body) {
|
||||
return new Response("No body provided", { status: 400 });
|
||||
}
|
||||
|
||||
const run = await $replica.taskRun.findFirst({
|
||||
where: {
|
||||
friendlyId: params.runId,
|
||||
runtimeEnvironmentId: authentication.environment.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
friendlyId: true,
|
||||
parentTaskRun: {
|
||||
select: {
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
rootTaskRun: {
|
||||
select: {
|
||||
friendlyId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return new Response("Run not found", { status: 404 });
|
||||
}
|
||||
|
||||
const targetId =
|
||||
params.target === "self"
|
||||
? run.friendlyId
|
||||
: params.target === "parent"
|
||||
? run.parentTaskRun?.friendlyId
|
||||
: run.rootTaskRun?.friendlyId;
|
||||
|
||||
if (!targetId) {
|
||||
return new Response("Target not found", { status: 404 });
|
||||
}
|
||||
|
||||
return relayRealtimeStreams.ingestData(request.body, targetId, params.streamId);
|
||||
}
|
||||
);
|
||||
|
||||
export { action };
|
||||
+1
-1
@@ -393,7 +393,7 @@ export function UpsertScheduleForm({
|
||||
variant="primary/medium"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
shortcut={{ key: "enter", modifiers: ["meta"] }}
|
||||
shortcut={{ key: "enter", modifiers: ["mod"] }}
|
||||
LeadingIcon={isLoading ? "spinner" : undefined}
|
||||
>
|
||||
{buttonText(mode, isLoading)}
|
||||
|
||||
+2
-2
@@ -201,7 +201,7 @@ function SpanBody({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="h-fit overflow-x-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<TabContainer>
|
||||
<TabButton
|
||||
isActive={!tab || tab === "overview"}
|
||||
@@ -442,7 +442,7 @@ function RunBody({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="h-fit overflow-x-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<TabContainer>
|
||||
<TabButton
|
||||
isActive={!tab || tab === "overview"}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ShieldCheckIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { ArrowDownCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Form, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { uiComponent } from "@team-plain/typescript-sdk";
|
||||
@@ -633,6 +633,11 @@ export function TierPro({
|
||||
const navigation = useNavigation();
|
||||
const formAction = `/resources/orgs/${organizationSlug}/select-plan`;
|
||||
const isLoading = navigation.formAction === formAction;
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDialogOpen(false);
|
||||
}, [subscription]);
|
||||
|
||||
return (
|
||||
<TierContainer>
|
||||
@@ -645,27 +650,67 @@ export function TierPro({
|
||||
<input type="hidden" name="type" value="paid" />
|
||||
<input type="hidden" name="planCode" value={plan.code} />
|
||||
<input type="hidden" name="callerPath" value={location.pathname} />
|
||||
<Button
|
||||
variant="tertiary/large"
|
||||
fullWidth
|
||||
form="subscribe-pro"
|
||||
className="text-md font-medium"
|
||||
disabled={
|
||||
isLoading ||
|
||||
(subscription?.plan?.code === plan.code && subscription.canceledAt === undefined)
|
||||
}
|
||||
LeadingIcon={
|
||||
isLoading && navigation.formData?.get("planCode") === plan.code ? Spinner : undefined
|
||||
}
|
||||
>
|
||||
{subscription?.plan === undefined
|
||||
? "Select plan"
|
||||
: subscription.plan.type === "free" || subscription.canceledAt !== undefined
|
||||
? `Upgrade to ${plan.title}`
|
||||
: subscription.plan.code === plan.code
|
||||
? "Current plan"
|
||||
: `Upgrade to ${plan.title}`}
|
||||
</Button>
|
||||
{subscription?.plan !== undefined &&
|
||||
subscription?.plan?.type === "paid" &&
|
||||
subscription?.plan?.code !== plan.code &&
|
||||
subscription.canceledAt === undefined ? (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen} key="upgrade">
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="tertiary/large" fullWidth className="text-md font-medium">
|
||||
{`Upgrade to ${plan.title}`}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>Upgrade plan</DialogHeader>
|
||||
<div className="mb-2 mt-4 flex items-start gap-3">
|
||||
<span>
|
||||
<ArrowUpCircleIcon className="size-12 text-primary" />
|
||||
</span>
|
||||
<Paragraph variant="base/bright" className="text-text-bright">
|
||||
Upgrade to get instant access to all the Pro features. You will be charged the
|
||||
new plan price for the remainder of this month on a pro rata basis.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="tertiary/medium" onClick={() => setIsDialogOpen(false)}>
|
||||
Dismiss
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary/medium"
|
||||
disabled={isLoading}
|
||||
LeadingIcon={isLoading ? () => <Spinner color="dark" /> : undefined}
|
||||
form="subscribe-pro"
|
||||
>
|
||||
{`Upgrade to ${plan.title}`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary/large"
|
||||
fullWidth
|
||||
form="subscribe-pro"
|
||||
className="text-md font-medium"
|
||||
disabled={
|
||||
isLoading ||
|
||||
(subscription?.plan?.code === plan.code && subscription.canceledAt === undefined)
|
||||
}
|
||||
LeadingIcon={
|
||||
isLoading && navigation.formData?.get("planCode") === plan.code
|
||||
? Spinner
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{subscription?.plan === undefined
|
||||
? "Select plan"
|
||||
: subscription.plan.type === "free" || subscription.canceledAt !== undefined
|
||||
? `Upgrade to ${plan.title}`
|
||||
: subscription.plan.code === plan.code
|
||||
? "Current plan"
|
||||
: `Upgrade to ${plan.title}`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function Story() {
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Header3 className="mb-1 uppercase">Shortcut</Header3>
|
||||
<Button variant="primary/small" shortcut={{ key: "s", modifiers: ["meta"] }}>
|
||||
<Button variant="primary/small" shortcut={{ key: "s", modifiers: ["mod"] }}>
|
||||
Primary button
|
||||
</Button>
|
||||
<Button variant="secondary/small" shortcut={{ key: "f" }}>
|
||||
@@ -82,7 +82,7 @@ export default function Story() {
|
||||
<Button variant="minimal/small" shortcut={{ key: "i" }}>
|
||||
Minimal button
|
||||
</Button>
|
||||
<Button variant="danger/small" shortcut={{ key: "s", modifiers: ["meta"] }}>
|
||||
<Button variant="danger/small" shortcut={{ key: "s", modifiers: ["mod"] }}>
|
||||
Danger button
|
||||
</Button>
|
||||
</div>
|
||||
@@ -193,19 +193,19 @@ export default function Story() {
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Header3 className="mb-1 uppercase">Shortcut</Header3>
|
||||
<Button variant="primary/medium" shortcut={{ key: "s", modifiers: ["meta"] }}>
|
||||
<Button variant="primary/medium" shortcut={{ key: "s", modifiers: ["mod"] }}>
|
||||
Primary button
|
||||
</Button>
|
||||
<Button variant="secondary/medium" shortcut={{ key: "s", modifiers: ["meta"] }}>
|
||||
<Button variant="secondary/medium" shortcut={{ key: "s", modifiers: ["mod"] }}>
|
||||
Secondary button
|
||||
</Button>
|
||||
<Button variant="tertiary/medium" shortcut={{ key: "s", modifiers: ["meta"] }}>
|
||||
<Button variant="tertiary/medium" shortcut={{ key: "s", modifiers: ["mod"] }}>
|
||||
Tertiary button
|
||||
</Button>
|
||||
<Button variant="minimal/medium" shortcut={{ key: "s", modifiers: ["meta"] }}>
|
||||
<Button variant="minimal/medium" shortcut={{ key: "s", modifiers: ["mod"] }}>
|
||||
Minimal button
|
||||
</Button>
|
||||
<Button variant="danger/medium" shortcut={{ key: "s", modifiers: ["meta"] }}>
|
||||
<Button variant="danger/medium" shortcut={{ key: "s", modifiers: ["mod"] }}>
|
||||
Danger button
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,13 @@ import { ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
const shortcuts: ShortcutDefinition[] = [
|
||||
{ key: "esc" },
|
||||
{ key: "f" },
|
||||
{ key: "f", modifiers: ["meta"] },
|
||||
{ key: "k", modifiers: ["meta"] },
|
||||
{ key: "f", modifiers: ["mod"] },
|
||||
{ key: "k", modifiers: ["mod"] },
|
||||
{ key: "del", modifiers: ["ctrl", "alt"] },
|
||||
{ key: "enter", modifiers: ["meta"] },
|
||||
{ key: "f", modifiers: ["shift"] },
|
||||
{ key: "enter", modifiers: ["mod"] },
|
||||
{ key: "enter", modifiers: ["mod"] },
|
||||
{ key: "g", modifiers: ["meta"] },
|
||||
];
|
||||
|
||||
export default function Story() {
|
||||
@@ -31,6 +33,7 @@ function Collection({ platform }: { platform: "mac" | "windows" }) {
|
||||
<div key={index} className="flex items-center gap-x-2">
|
||||
<ShortcutKey shortcut={shortcut} variant="small" />
|
||||
<ShortcutKey shortcut={shortcut} variant="medium" />
|
||||
<ShortcutKey shortcut={shortcut} variant="medium/bright" />
|
||||
<Button variant="primary/small" shortcut={shortcut}>
|
||||
Primary small
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
|
||||
export default function Story() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-3 p-4">
|
||||
<div className="flex items-center gap-x-4 rounded-md bg-charcoal-750 px-3 py-2 text-text-bright">
|
||||
Blue: <Spinner color="blue" />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4 rounded-md bg-charcoal-750 px-3 py-2 text-text-bright">
|
||||
White: <Spinner color="white" />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4 rounded-md bg-charcoal-600 px-3 py-2 text-text-bright">
|
||||
Muted: <Spinner color="muted" />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-center gap-x-4 rounded-md bg-charcoal-600 px-3 py-2 text-text-bright">
|
||||
Dark: <Spinner color="dark" />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4 rounded-md bg-primary px-2 py-2 text-text-bright">
|
||||
<Spinner color="dark" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4 rounded-md bg-charcoal-600 px-3 py-2 text-text-bright">
|
||||
Custom: <Spinner color={{ background: "#EA189E", foreground: "#6532F5" }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,15 +6,20 @@ export default function Story() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-4 p-4">
|
||||
<ToastUI variant="success" message="Success UI" t="-" />
|
||||
<ToastUI variant="error" message="Error UI" t="-" />
|
||||
<ToastUI
|
||||
variant="success"
|
||||
message="This is a long success message that wraps over multiple lines so we can test the UI."
|
||||
t="-"
|
||||
/>
|
||||
<ToastUI variant="error" message="Error UI" t="-" />
|
||||
<ToastUI
|
||||
variant="error"
|
||||
message="This is a long error message that wraps over multiple lines so we can test the UI."
|
||||
t="-"
|
||||
/>
|
||||
<br />
|
||||
<Button
|
||||
variant="primary/large"
|
||||
variant="primary/medium"
|
||||
onClick={() =>
|
||||
toast.custom((t) => <ToastUI variant="success" message="Success" t={t as string} />, {
|
||||
duration: Infinity, // Prevents auto-dismissal for demo purposes
|
||||
@@ -24,7 +29,7 @@ export default function Story() {
|
||||
Trigger success toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger/large"
|
||||
variant="danger/medium"
|
||||
onClick={() =>
|
||||
toast.custom((t) => <ToastUI variant="error" message="Error" t={t as string} />, {
|
||||
duration: Infinity,
|
||||
|
||||
@@ -84,6 +84,10 @@ const stories: Story[] = [
|
||||
name: "Shortcuts",
|
||||
slug: "shortcuts",
|
||||
},
|
||||
{
|
||||
name: "Spinners",
|
||||
slug: "spinner",
|
||||
},
|
||||
{
|
||||
name: "Switch",
|
||||
slug: "switch",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
isPersonalAccessToken,
|
||||
} from "./personalAccessToken.server";
|
||||
import { isPublicJWT, validatePublicJwtKey } from "./realtime/jwtAuth.server";
|
||||
import { RuntimeEnvironmentForEnvRepo } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
||||
|
||||
const ClaimsSchema = z.object({
|
||||
scopes: z.array(z.string()).optional(),
|
||||
@@ -410,7 +411,7 @@ const JWT_ALGORITHM = "HS256";
|
||||
const DEFAULT_JWT_EXPIRATION_IN_MS = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
export async function generateJWTTokenForEnvironment(
|
||||
environment: RuntimeEnvironment,
|
||||
environment: RuntimeEnvironmentForEnvRepo,
|
||||
payload: Record<string, string>
|
||||
) {
|
||||
const jwt = await new SignJWT({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DeliverEmail, SendPlainTextOptions } from "emails";
|
||||
import { EmailClient } from "emails";
|
||||
import { EmailClient, MailTransportOptions } from "emails";
|
||||
import type { SendEmailOptions } from "remix-auth-email-link";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { env } from "~/env.server";
|
||||
@@ -13,7 +13,7 @@ const client = singleton(
|
||||
"email-client",
|
||||
() =>
|
||||
new EmailClient({
|
||||
apikey: env.RESEND_API_KEY,
|
||||
transport: buildTransportOptions(),
|
||||
imagesBaseUrl: env.APP_ORIGIN,
|
||||
from: env.FROM_EMAIL ?? "team@email.trigger.dev",
|
||||
replyTo: env.REPLY_TO_EMAIL ?? "help@email.trigger.dev",
|
||||
@@ -24,13 +24,45 @@ const alertsClient = singleton(
|
||||
"alerts-email-client",
|
||||
() =>
|
||||
new EmailClient({
|
||||
apikey: env.ALERT_RESEND_API_KEY,
|
||||
transport: buildTransportOptions(true),
|
||||
imagesBaseUrl: env.APP_ORIGIN,
|
||||
from: env.ALERT_FROM_EMAIL ?? "noreply@alerts.trigger.dev",
|
||||
replyTo: env.REPLY_TO_EMAIL ?? "help@email.trigger.dev",
|
||||
})
|
||||
);
|
||||
|
||||
function buildTransportOptions(alerts?: boolean): MailTransportOptions {
|
||||
const transportType = alerts ? env.ALERT_EMAIL_TRANSPORT : env.EMAIL_TRANSPORT
|
||||
logger.debug(`Constructing email transport '${transportType}' for usage '${alerts?'alerts':'general'}'`)
|
||||
|
||||
switch (transportType) {
|
||||
case "aws-ses":
|
||||
return { type: "aws-ses" };
|
||||
case "resend":
|
||||
return {
|
||||
type: "resend",
|
||||
config: {
|
||||
apiKey: alerts ? env.ALERT_RESEND_API_KEY : env.RESEND_API_KEY,
|
||||
}
|
||||
}
|
||||
case "smtp":
|
||||
return {
|
||||
type: "smtp",
|
||||
config: {
|
||||
host: alerts ? env.ALERT_SMTP_HOST : env.SMTP_HOST,
|
||||
port: alerts ? env.ALERT_SMTP_PORT : env.SMTP_PORT,
|
||||
secure: alerts ? env.ALERT_SMTP_SECURE : env.SMTP_SECURE,
|
||||
auth: {
|
||||
user: alerts ? env.ALERT_SMTP_USER : env.SMTP_USER,
|
||||
pass: alerts ? env.ALERT_SMTP_PASSWORD : env.SMTP_PASSWORD
|
||||
}
|
||||
}
|
||||
};
|
||||
default:
|
||||
return { type: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendMagicLinkEmail(options: SendEmailOptions<AuthUser>): Promise<void> {
|
||||
// Auto redirect when in development mode
|
||||
if (env.NODE_ENV === "development") {
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
import {
|
||||
applyMetadataOperations,
|
||||
IOPacket,
|
||||
parsePacket,
|
||||
RunMetadataChangeOperation,
|
||||
UpdateMetadataRequestBody,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { prisma, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { handleMetadataPacket } from "~/utils/packets";
|
||||
import { BaseService, ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import { isFinalRunStatus } from "~/v3/taskStatus";
|
||||
|
||||
import { Effect, Schedule, Duration } from "effect";
|
||||
import { type RuntimeFiber } from "effect/Fiber";
|
||||
import { logger } from "../logger.server";
|
||||
import { singleton } from "~/utils/singleton";
|
||||
import { env } from "~/env.server";
|
||||
import { setTimeout } from "timers/promises";
|
||||
|
||||
type BufferedRunMetadataChangeOperation = {
|
||||
runId: string;
|
||||
timestamp: number;
|
||||
operation: RunMetadataChangeOperation;
|
||||
};
|
||||
|
||||
export class UpdateMetadataService extends BaseService {
|
||||
private _bufferedOperations: Map<string, BufferedRunMetadataChangeOperation[]> = new Map();
|
||||
private _flushFiber: RuntimeFiber<void> | null = null;
|
||||
|
||||
constructor(
|
||||
protected readonly _prisma: PrismaClientOrTransaction = prisma,
|
||||
private readonly flushIntervalMs: number = 5000,
|
||||
private readonly flushEnabled: boolean = true,
|
||||
private readonly flushLoggingEnabled: boolean = true
|
||||
) {
|
||||
super();
|
||||
|
||||
this._startFlushing();
|
||||
}
|
||||
|
||||
// Start a loop that periodically flushes buffered operations
|
||||
private _startFlushing() {
|
||||
if (!this.flushEnabled) {
|
||||
logger.info("[UpdateMetadataService] 🚽 Flushing disabled");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("[UpdateMetadataService] 🚽 Flushing started");
|
||||
|
||||
// Create a program that sleeps, then processes buffered ops
|
||||
const program = Effect.gen(this, function* (_) {
|
||||
while (true) {
|
||||
// Wait for flushIntervalMs before flushing again
|
||||
yield* _(Effect.sleep(Duration.millis(this.flushIntervalMs)));
|
||||
|
||||
// Atomically get and clear current operations
|
||||
const currentOperations = new Map(this._bufferedOperations);
|
||||
this._bufferedOperations.clear();
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
if (this.flushLoggingEnabled) {
|
||||
logger.debug(`[UpdateMetadataService] Flushing operations`, {
|
||||
operations: Object.fromEntries(currentOperations),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If we have operations, process them
|
||||
if (currentOperations.size > 0) {
|
||||
yield* _(this._processBufferedOperations(currentOperations));
|
||||
}
|
||||
}
|
||||
}).pipe(
|
||||
// Handle any unexpected errors, ensuring program does not fail
|
||||
Effect.catchAll((error) =>
|
||||
Effect.sync(() => {
|
||||
logger.error("Error in flushing program:", { error });
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Fork the program so it runs in the background
|
||||
this._flushFiber = Effect.runFork(program as Effect.Effect<void, never, never>);
|
||||
}
|
||||
|
||||
private _processBufferedOperations = (
|
||||
operations: Map<string, BufferedRunMetadataChangeOperation[]>
|
||||
) => {
|
||||
return Effect.gen(this, function* (_) {
|
||||
for (const [runId, ops] of operations) {
|
||||
// Process and cull operations
|
||||
const processedOps = this._cullOperations(ops);
|
||||
|
||||
// If there are no operations to process, skip
|
||||
if (processedOps.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield* Effect.sync(() => {
|
||||
if (this.flushLoggingEnabled) {
|
||||
logger.debug(`[UpdateMetadataService] Processing operations for run`, {
|
||||
runId,
|
||||
operationsCount: processedOps.length,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update run with retry
|
||||
yield* _(
|
||||
this._updateRunWithOperations(runId, processedOps).pipe(
|
||||
Effect.retry(Schedule.exponential(Duration.millis(100), 1.4)),
|
||||
Effect.catchAll((error) =>
|
||||
Effect.sync(() => {
|
||||
// On complete failure, return ops to buffer
|
||||
const existingOps = this._bufferedOperations.get(runId) ?? [];
|
||||
this._bufferedOperations.set(runId, [...existingOps, ...ops]);
|
||||
console.error(`Failed to process run ${runId}:`, error);
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private _updateRunWithOperations = (
|
||||
runId: string,
|
||||
operations: BufferedRunMetadataChangeOperation[]
|
||||
) => {
|
||||
return Effect.gen(this, function* (_) {
|
||||
// Fetch current run
|
||||
const run = yield* _(
|
||||
Effect.tryPromise(() =>
|
||||
this._prisma.taskRun.findFirst({
|
||||
where: { id: runId },
|
||||
select: { id: true, metadata: true, metadataType: true, metadataVersion: true },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (!run) {
|
||||
return yield* _(Effect.fail(new Error(`Run ${runId} not found`)));
|
||||
}
|
||||
|
||||
const metadata = yield* _(
|
||||
Effect.tryPromise(() =>
|
||||
run.metadata
|
||||
? parsePacket({ data: run.metadata, dataType: run.metadataType })
|
||||
: Promise.resolve({})
|
||||
)
|
||||
);
|
||||
|
||||
// Apply operations and update
|
||||
const applyResult = applyMetadataOperations(
|
||||
metadata,
|
||||
operations.map((op) => op.operation)
|
||||
);
|
||||
|
||||
if (applyResult.unappliedOperations.length === operations.length) {
|
||||
logger.warn(`No operations applied for run ${runId}`);
|
||||
// If no operations were applied, return
|
||||
return;
|
||||
}
|
||||
|
||||
// Stringify the metadata
|
||||
const newMetadataPacket = yield* _(
|
||||
Effect.try(() => handleMetadataPacket(applyResult.newMetadata, run.metadataType))
|
||||
);
|
||||
|
||||
if (!newMetadataPacket) {
|
||||
// Log and skip if metadata is invalid
|
||||
logger.warn(`Invalid metadata after operations, skipping update`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = yield* _(
|
||||
Effect.tryPromise(() =>
|
||||
this._prisma.taskRun.updateMany({
|
||||
where: {
|
||||
id: runId,
|
||||
metadataVersion: run.metadataVersion,
|
||||
},
|
||||
data: {
|
||||
metadata: newMetadataPacket.data,
|
||||
metadataVersion: { increment: 1 },
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (result.count === 0) {
|
||||
yield* Effect.sync(() => {
|
||||
logger.warn(`Optimistic lock failed for run ${runId}`, {
|
||||
metadataVersion: run.metadataVersion,
|
||||
});
|
||||
});
|
||||
|
||||
return yield* _(Effect.fail(new Error("Optimistic lock failed")));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
private _cullOperations(
|
||||
operations: BufferedRunMetadataChangeOperation[]
|
||||
): BufferedRunMetadataChangeOperation[] {
|
||||
// Sort by timestamp
|
||||
const sortedOps = [...operations].sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
// Track latest set operations by key
|
||||
const latestSetOps = new Map<string, BufferedRunMetadataChangeOperation>();
|
||||
const resultOps: BufferedRunMetadataChangeOperation[] = [];
|
||||
|
||||
for (const op of sortedOps) {
|
||||
if (op.operation.type === "set") {
|
||||
latestSetOps.set(op.operation.key, op);
|
||||
} else {
|
||||
resultOps.push(op);
|
||||
}
|
||||
}
|
||||
|
||||
// Add winning set operations
|
||||
resultOps.push(...latestSetOps.values());
|
||||
|
||||
return resultOps;
|
||||
}
|
||||
|
||||
public async call(
|
||||
runId: string,
|
||||
body: UpdateMetadataRequestBody,
|
||||
environment?: AuthenticatedEnvironment
|
||||
) {
|
||||
const runIdType = runId.startsWith("run_") ? "friendly" : "internal";
|
||||
|
||||
const taskRun = await this._prisma.taskRun.findFirst({
|
||||
where: environment
|
||||
? {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
...(runIdType === "internal" ? { id: runId } : { friendlyId: runId }),
|
||||
}
|
||||
: {
|
||||
...(runIdType === "internal" ? { id: runId } : { friendlyId: runId }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
metadata: true,
|
||||
metadataType: true,
|
||||
metadataVersion: true,
|
||||
parentTaskRun: {
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
rootTaskRun: {
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!taskRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFinalRunStatus(taskRun.status)) {
|
||||
throw new ServiceValidationError("Cannot update metadata for a completed run");
|
||||
}
|
||||
|
||||
if (body.parentOperations && body.parentOperations.length > 0 && taskRun.parentTaskRun) {
|
||||
this.#ingestRunOperations(taskRun.parentTaskRun.id, body.parentOperations);
|
||||
}
|
||||
|
||||
if (body.rootOperations && body.rootOperations.length > 0 && taskRun.rootTaskRun) {
|
||||
this.#ingestRunOperations(taskRun.rootTaskRun.id, body.rootOperations);
|
||||
}
|
||||
|
||||
const newMetadata = await this.#updateRunMetadata({
|
||||
runId: taskRun.id,
|
||||
body,
|
||||
existingMetadata: {
|
||||
data: taskRun.metadata ?? undefined,
|
||||
dataType: taskRun.metadataType,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
metadata: newMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
async #updateRunMetadata({
|
||||
runId,
|
||||
body,
|
||||
existingMetadata,
|
||||
}: {
|
||||
runId: string;
|
||||
body: UpdateMetadataRequestBody;
|
||||
existingMetadata: IOPacket;
|
||||
}) {
|
||||
if (Array.isArray(body.operations)) {
|
||||
return this.#updateRunMetadataWithOperations(runId, body.operations);
|
||||
} else {
|
||||
return this.#updateRunMetadataDirectly(runId, body, existingMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
async #updateRunMetadataWithOperations(runId: string, operations: RunMetadataChangeOperation[]) {
|
||||
const MAX_RETRIES = 3;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts <= MAX_RETRIES) {
|
||||
// Fetch the latest run data
|
||||
const run = await this._prisma.taskRun.findFirst({
|
||||
where: { id: runId },
|
||||
select: { metadata: true, metadataType: true, metadataVersion: true },
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
throw new Error(`Run ${runId} not found`);
|
||||
}
|
||||
|
||||
// Parse the current metadata
|
||||
const currentMetadata = await (run.metadata
|
||||
? parsePacket({ data: run.metadata, dataType: run.metadataType })
|
||||
: Promise.resolve({}));
|
||||
|
||||
// Apply operations to the current metadata
|
||||
const applyResults = applyMetadataOperations(currentMetadata, operations);
|
||||
|
||||
// If no operations were applied, return the current metadata
|
||||
if (applyResults.unappliedOperations.length === operations.length) {
|
||||
return currentMetadata;
|
||||
}
|
||||
|
||||
// Update with optimistic locking
|
||||
const result = await this._prisma.taskRun.updateMany({
|
||||
where: {
|
||||
id: runId,
|
||||
metadataVersion: run.metadataVersion,
|
||||
},
|
||||
data: {
|
||||
metadata: JSON.stringify(applyResults.newMetadata),
|
||||
metadataType: run.metadataType,
|
||||
metadataVersion: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.count === 0) {
|
||||
if (this.flushLoggingEnabled) {
|
||||
logger.debug(
|
||||
`[UpdateMetadataService][updateRunMetadataWithOperations] Optimistic lock failed for run ${runId}`,
|
||||
{
|
||||
metadataVersion: run.metadataVersion,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// If this was our last attempt, buffer the operations and return optimistically
|
||||
if (attempts === MAX_RETRIES) {
|
||||
this.#ingestRunOperations(runId, operations);
|
||||
return applyResults.newMetadata;
|
||||
}
|
||||
|
||||
// Otherwise sleep and try again
|
||||
await setTimeout(100 * Math.pow(1.4, attempts));
|
||||
attempts++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.flushLoggingEnabled) {
|
||||
logger.debug(
|
||||
`[UpdateMetadataService][updateRunMetadataWithOperations] Updated metadata for run ${runId}`,
|
||||
{
|
||||
metadata: applyResults.newMetadata,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Success! Return the new metadata
|
||||
return applyResults.newMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
async #updateRunMetadataDirectly(
|
||||
runId: string,
|
||||
body: UpdateMetadataRequestBody,
|
||||
existingMetadata: IOPacket
|
||||
) {
|
||||
const metadataPacket = handleMetadataPacket(body.metadata, "application/json");
|
||||
|
||||
if (!metadataPacket) {
|
||||
throw new ServiceValidationError("Invalid metadata");
|
||||
}
|
||||
|
||||
if (
|
||||
metadataPacket.data !== "{}" ||
|
||||
(existingMetadata.data && metadataPacket.data !== existingMetadata.data)
|
||||
) {
|
||||
if (this.flushLoggingEnabled) {
|
||||
logger.debug(
|
||||
`[UpdateMetadataService][updateRunMetadataDirectly] Updating metadata directly for run`,
|
||||
{
|
||||
metadata: metadataPacket.data,
|
||||
runId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Update the metadata without version check
|
||||
await this._prisma.taskRun.update({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
data: {
|
||||
metadata: metadataPacket?.data,
|
||||
metadataType: metadataPacket?.dataType,
|
||||
metadataVersion: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const newMetadata = await parsePacket(metadataPacket);
|
||||
return newMetadata;
|
||||
}
|
||||
|
||||
#ingestRunOperations(runId: string, operations: RunMetadataChangeOperation[]) {
|
||||
const bufferedOperations: BufferedRunMetadataChangeOperation[] = operations.map((operation) => {
|
||||
return {
|
||||
runId,
|
||||
timestamp: Date.now(),
|
||||
operation,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.flushLoggingEnabled) {
|
||||
logger.debug(`[UpdateMetadataService] Ingesting operations for run`, {
|
||||
runId,
|
||||
bufferedOperations,
|
||||
});
|
||||
}
|
||||
|
||||
const existingBufferedOperations = this._bufferedOperations.get(runId) ?? [];
|
||||
|
||||
this._bufferedOperations.set(runId, [...existingBufferedOperations, ...bufferedOperations]);
|
||||
}
|
||||
}
|
||||
|
||||
export const updateMetadataService = singleton(
|
||||
"update-metadata-service",
|
||||
() =>
|
||||
new UpdateMetadataService(
|
||||
prisma,
|
||||
env.BATCH_METADATA_OPERATIONS_FLUSH_INTERVAL_MS,
|
||||
env.BATCH_METADATA_OPERATIONS_FLUSH_ENABLED === "1",
|
||||
env.BATCH_METADATA_OPERATIONS_FLUSH_LOGGING_ENABLED === "1"
|
||||
)
|
||||
);
|
||||
@@ -37,13 +37,14 @@ export class DatabaseRealtimeStreams implements StreamIngestor, StreamResponder
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const textStream = stream.pipeThrough(new TextDecoderStream());
|
||||
|
||||
const reader = textStream.getReader();
|
||||
let sequence = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
if (done || !value) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -53,25 +54,13 @@ export class DatabaseRealtimeStreams implements StreamIngestor, StreamResponder
|
||||
value,
|
||||
});
|
||||
|
||||
const chunks = value
|
||||
.split("\n")
|
||||
.filter((chunk) => chunk) // Remove empty lines
|
||||
.map((line) => {
|
||||
return {
|
||||
sequence: sequence++,
|
||||
value: line,
|
||||
};
|
||||
});
|
||||
|
||||
await this.options.prisma.realtimeStreamChunk.createMany({
|
||||
data: chunks.map((chunk) => {
|
||||
return {
|
||||
runId,
|
||||
key: streamId,
|
||||
sequence: chunk.sequence,
|
||||
value: chunk.value,
|
||||
};
|
||||
}),
|
||||
await this.options.prisma.realtimeStreamChunk.create({
|
||||
data: {
|
||||
runId,
|
||||
key: streamId,
|
||||
sequence: sequence++,
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Redis, { RedisKey, RedisOptions, RedisValue } from "ioredis";
|
||||
import Redis, { RedisOptions } from "ioredis";
|
||||
import { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { StreamIngestor, StreamResponder } from "./types";
|
||||
import { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { LineTransformStream } from "./utils.server";
|
||||
|
||||
export type RealtimeStreamsOptions = {
|
||||
redis: RedisOptions | undefined;
|
||||
@@ -56,7 +57,7 @@ export class RedisRealtimeStreams implements StreamIngestor, StreamResponder {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(`data: ${fields[1]}\n\n`);
|
||||
controller.enqueue(fields[1]);
|
||||
|
||||
if (signal.aborted) {
|
||||
controller.close();
|
||||
@@ -88,7 +89,18 @@ export class RedisRealtimeStreams implements StreamIngestor, StreamResponder {
|
||||
cancel: async () => {
|
||||
await cleanup();
|
||||
},
|
||||
});
|
||||
})
|
||||
.pipeThrough(new LineTransformStream())
|
||||
.pipeThrough(
|
||||
new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
for (const line of chunk) {
|
||||
controller.enqueue(`data: ${line}\n\n`);
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
.pipeThrough(new TextEncoderStream());
|
||||
|
||||
async function cleanup() {
|
||||
if (isCleanedUp) return;
|
||||
@@ -98,7 +110,7 @@ export class RedisRealtimeStreams implements StreamIngestor, StreamResponder {
|
||||
|
||||
signal.addEventListener("abort", cleanup);
|
||||
|
||||
return new Response(stream.pipeThrough(new TextEncoderStream()), {
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
@@ -119,7 +131,7 @@ export class RedisRealtimeStreams implements StreamIngestor, StreamResponder {
|
||||
try {
|
||||
await redis.quit();
|
||||
} catch (error) {
|
||||
logger.error("[RealtimeStreams][ingestData] Error in cleanup:", { error });
|
||||
logger.error("[RedisRealtimeStreams][ingestData] Error in cleanup:", { error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,42 +139,20 @@ export class RedisRealtimeStreams implements StreamIngestor, StreamResponder {
|
||||
const textStream = stream.pipeThrough(new TextDecoderStream());
|
||||
const reader = textStream.getReader();
|
||||
|
||||
const batchSize = 10;
|
||||
let batchCommands: Array<[key: RedisKey, ...args: RedisValue[]]> = [];
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
if (done || !value) {
|
||||
break;
|
||||
}
|
||||
|
||||
logger.debug("[RealtimeStreams][ingestData] Reading data", { streamKey, value });
|
||||
logger.debug("[RedisRealtimeStreams][ingestData] Reading data", {
|
||||
streamKey,
|
||||
runId,
|
||||
value,
|
||||
});
|
||||
|
||||
const lines = value.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
batchCommands.push([streamKey, "MAXLEN", "~", "2500", "*", "data", line]);
|
||||
|
||||
if (batchCommands.length >= batchSize) {
|
||||
const pipeline = redis.pipeline();
|
||||
for (const args of batchCommands) {
|
||||
pipeline.xadd(...args);
|
||||
}
|
||||
await pipeline.exec();
|
||||
batchCommands = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (batchCommands.length > 0) {
|
||||
const pipeline = redis.pipeline();
|
||||
for (const args of batchCommands) {
|
||||
pipeline.xadd(...args);
|
||||
}
|
||||
await pipeline.exec();
|
||||
await redis.xadd(streamKey, "MAXLEN", "~", "1000", "*", "data", value);
|
||||
}
|
||||
|
||||
await redis.xadd(streamKey, "MAXLEN", "~", "1000", "*", "data", END_SENTINEL);
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { StreamIngestor, StreamResponder } from "./types";
|
||||
import { LineTransformStream } from "./utils.server";
|
||||
import { v1RealtimeStreams } from "./v1StreamsGlobal.server";
|
||||
import { singleton } from "~/utils/singleton";
|
||||
|
||||
export type RelayRealtimeStreamsOptions = {
|
||||
ttl: number;
|
||||
cleanupInterval: number;
|
||||
fallbackIngestor: StreamIngestor;
|
||||
fallbackResponder: StreamResponder;
|
||||
waitForBufferTimeout?: number; // Time to wait for buffer in ms (default: 500ms)
|
||||
waitForBufferInterval?: number; // Polling interval in ms (default: 50ms)
|
||||
};
|
||||
|
||||
interface RelayedStreamRecord {
|
||||
stream: ReadableStream<Uint8Array>;
|
||||
createdAt: number;
|
||||
lastAccessed: number;
|
||||
locked: boolean;
|
||||
finalized: boolean;
|
||||
}
|
||||
|
||||
export class RelayRealtimeStreams implements StreamIngestor, StreamResponder {
|
||||
private _buffers: Map<string, RelayedStreamRecord> = new Map();
|
||||
private cleanupInterval: NodeJS.Timeout;
|
||||
private waitForBufferTimeout: number;
|
||||
private waitForBufferInterval: number;
|
||||
|
||||
constructor(private options: RelayRealtimeStreamsOptions) {
|
||||
this.waitForBufferTimeout = options.waitForBufferTimeout ?? 1200;
|
||||
this.waitForBufferInterval = options.waitForBufferInterval ?? 50;
|
||||
|
||||
// Periodic cleanup
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cleanup();
|
||||
}, this.options.cleanupInterval).unref();
|
||||
}
|
||||
|
||||
async streamResponse(
|
||||
request: Request,
|
||||
runId: string,
|
||||
streamId: string,
|
||||
environment: AuthenticatedEnvironment,
|
||||
signal: AbortSignal
|
||||
): Promise<Response> {
|
||||
let record = this._buffers.get(`${runId}:${streamId}`);
|
||||
|
||||
if (!record) {
|
||||
logger.debug(
|
||||
"[RelayRealtimeStreams][streamResponse] No ephemeral record found, waiting to see if one becomes available",
|
||||
{
|
||||
streamId,
|
||||
runId,
|
||||
}
|
||||
);
|
||||
|
||||
record = await this.waitForBuffer(`${runId}:${streamId}`);
|
||||
|
||||
if (!record) {
|
||||
logger.debug(
|
||||
"[RelayRealtimeStreams][streamResponse] No ephemeral record found, using fallback",
|
||||
{
|
||||
streamId,
|
||||
runId,
|
||||
}
|
||||
);
|
||||
|
||||
// No ephemeral record, use fallback
|
||||
return this.options.fallbackResponder.streamResponse(
|
||||
request,
|
||||
runId,
|
||||
streamId,
|
||||
environment,
|
||||
signal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Only 1 reader of the stream can use the relayed stream, the rest should use the fallback
|
||||
if (record.locked) {
|
||||
logger.debug("[RelayRealtimeStreams][streamResponse] Stream already locked, using fallback", {
|
||||
streamId,
|
||||
runId,
|
||||
});
|
||||
|
||||
return this.options.fallbackResponder.streamResponse(
|
||||
request,
|
||||
runId,
|
||||
streamId,
|
||||
environment,
|
||||
signal
|
||||
);
|
||||
}
|
||||
|
||||
record.locked = true;
|
||||
record.lastAccessed = Date.now();
|
||||
|
||||
logger.debug("[RelayRealtimeStreams][streamResponse] Streaming from ephemeral record", {
|
||||
streamId,
|
||||
runId,
|
||||
});
|
||||
|
||||
// Create a streaming response from the buffered data
|
||||
const stream = record.stream
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new LineTransformStream())
|
||||
.pipeThrough(
|
||||
new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
for (const line of chunk) {
|
||||
controller.enqueue(`data: ${line}\n\n`);
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
.pipeThrough(new TextEncoderStream());
|
||||
|
||||
// Once we start streaming, consider deleting the buffer when done.
|
||||
// For a simple approach, we can rely on finalized and no more reads.
|
||||
// Or we can let TTL cleanup handle it if multiple readers might come in.
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"x-trigger-relay-realtime-streams": "true",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async ingestData(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
runId: string,
|
||||
streamId: string
|
||||
): Promise<Response> {
|
||||
const [localStream, fallbackStream] = stream.tee();
|
||||
|
||||
logger.debug("[RelayRealtimeStreams][ingestData] Ingesting data", { runId, streamId });
|
||||
|
||||
// Handle local buffering asynchronously and catch errors
|
||||
this.handleLocalIngestion(localStream, runId, streamId).catch((err) => {
|
||||
logger.error("[RelayRealtimeStreams][ingestData] Error in local ingestion:", { err });
|
||||
});
|
||||
|
||||
// Forward to the fallback ingestor asynchronously and catch errors
|
||||
return this.options.fallbackIngestor.ingestData(fallbackStream, runId, streamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles local buffering of the stream data.
|
||||
* @param stream The readable stream to buffer.
|
||||
* @param streamId The unique identifier for the stream.
|
||||
*/
|
||||
private async handleLocalIngestion(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
runId: string,
|
||||
streamId: string
|
||||
) {
|
||||
this.createOrUpdateRelayedStream(`${runId}:${streamId}`, stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an existing buffer or creates a new one for the given streamId.
|
||||
* @param streamId The unique identifier for the stream.
|
||||
*/
|
||||
private createOrUpdateRelayedStream(
|
||||
bufferKey: string,
|
||||
stream: ReadableStream<Uint8Array>
|
||||
): RelayedStreamRecord {
|
||||
let record = this._buffers.get(bufferKey);
|
||||
if (!record) {
|
||||
record = {
|
||||
stream,
|
||||
createdAt: Date.now(),
|
||||
lastAccessed: Date.now(),
|
||||
finalized: false,
|
||||
locked: false,
|
||||
};
|
||||
this._buffers.set(bufferKey, record);
|
||||
} else {
|
||||
record.lastAccessed = Date.now();
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
const now = Date.now();
|
||||
|
||||
logger.debug("[RelayRealtimeStreams][cleanup] Cleaning up old buffers", {
|
||||
bufferCount: this._buffers.size,
|
||||
});
|
||||
|
||||
for (const [key, record] of this._buffers.entries()) {
|
||||
// If last accessed is older than ttl, clean up
|
||||
if (now - record.lastAccessed > this.options.ttl) {
|
||||
this.deleteBuffer(key);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("[RelayRealtimeStreams][cleanup] Cleaned up old buffers", {
|
||||
bufferCount: this._buffers.size,
|
||||
});
|
||||
}
|
||||
|
||||
private deleteBuffer(bufferKey: string) {
|
||||
this._buffers.delete(bufferKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a buffer to be created within a specified timeout.
|
||||
* @param streamId The unique identifier for the stream.
|
||||
* @returns A promise that resolves to true if the buffer was created, false otherwise.
|
||||
*/
|
||||
private async waitForBuffer(bufferKey: string): Promise<RelayedStreamRecord | undefined> {
|
||||
const timeout = this.waitForBufferTimeout;
|
||||
const interval = this.waitForBufferInterval;
|
||||
const maxAttempts = Math.ceil(timeout / interval);
|
||||
let attempts = 0;
|
||||
|
||||
return new Promise<RelayedStreamRecord | undefined>((resolve) => {
|
||||
const checkBuffer = () => {
|
||||
attempts++;
|
||||
if (this._buffers.has(bufferKey)) {
|
||||
resolve(this._buffers.get(bufferKey));
|
||||
return;
|
||||
}
|
||||
if (attempts >= maxAttempts) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
setTimeout(checkBuffer, interval);
|
||||
};
|
||||
checkBuffer();
|
||||
});
|
||||
}
|
||||
|
||||
// Don't forget to clear interval on shutdown if needed
|
||||
close() {
|
||||
clearInterval(this.cleanupInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeRelayRealtimeStreams() {
|
||||
return new RelayRealtimeStreams({
|
||||
ttl: 1000 * 60 * 5, // 5 minutes
|
||||
cleanupInterval: 1000 * 60, // 1 minute
|
||||
fallbackIngestor: v1RealtimeStreams,
|
||||
fallbackResponder: v1RealtimeStreams,
|
||||
});
|
||||
}
|
||||
|
||||
export const relayRealtimeStreams = singleton(
|
||||
"relayRealtimeStreams",
|
||||
initializeRelayRealtimeStreams
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
export class LineTransformStream extends TransformStream<string, string[]> {
|
||||
private buffer = "";
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
transform: (chunk, controller) => {
|
||||
// Append the chunk to the buffer
|
||||
this.buffer += chunk;
|
||||
|
||||
// Split on newlines
|
||||
const lines = this.buffer.split("\n");
|
||||
|
||||
// The last element might be incomplete, hold it back in buffer
|
||||
this.buffer = lines.pop() || "";
|
||||
|
||||
// Filter out empty or whitespace-only lines
|
||||
const fullLines = lines.filter((line) => line.trim().length > 0);
|
||||
|
||||
// If we got any complete lines, emit them as an array
|
||||
if (fullLines.length > 0) {
|
||||
controller.enqueue(fullLines);
|
||||
}
|
||||
},
|
||||
flush: (controller) => {
|
||||
// On stream end, if there's leftover text, emit it as a single-element array
|
||||
const trimmed = this.buffer.trim();
|
||||
if (trimmed.length > 0) {
|
||||
controller.enqueue([trimmed]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user