Compare commits
338 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 30ea5eb13a | |||
| 5846f30228 | |||
| 3afa42c209 | |||
| 86b1628953 | |||
| ea23dbd297 | |||
| 9065e64be8 | |||
| 9970b9b68e | |||
| b4113134ad | |||
| 2a07ea42f1 | |||
| 91afa5ebbf | |||
| 65262dc3d7 | |||
| 9105701ae0 | |||
| 9b35cc484b | |||
| 30a04a5a06 | |||
| 493315af48 | |||
| 8db1da69e9 | |||
| cd7a45101e | |||
| 29d107dc0a | |||
| cf7dc8d719 | |||
| 9ced599b19 | |||
| 67592ec2b4 | |||
| f7bf7bc268 | |||
| bb57426a0d | |||
| 3ab7eb9c7a | |||
| 2892efad04 | |||
| 979ba51d2f | |||
| 364ea565ed | |||
| 58252728f6 | |||
| 5eaad0577e | |||
| 00f1103deb | |||
| c37622e7b6 | |||
| d67023aa8f | |||
| d9bfe55a8c | |||
| f4a18feca0 | |||
| 0acd052061 | |||
| 6bf3bbcfd7 | |||
| 1077709e15 | |||
| 54017cbffa | |||
| 39ef733a34 | |||
| 736f577c25 | |||
| b7de02ec14 | |||
| 90db0f6e01 | |||
| 6df8069c0e | |||
| b714d6fabf | |||
| 576715374e | |||
| 9046ea4f23 | |||
| 7e209e0771 | |||
| 3792394e5c | |||
| 4305a23668 | |||
| e7fc592cd9 | |||
| cb12a73db6 | |||
| 31fc9e12b7 | |||
| 2bdbedcdd8 | |||
| 332166a098 | |||
| 14f19066b5 | |||
| af7a7681cf | |||
| c044cb125e | |||
| 8ba226b99a | |||
| 7d4708839f | |||
| 532fdcad47 | |||
| 186942de5d | |||
| 23b43be952 | |||
| ea0956464b | |||
| eca79afced | |||
| 58861a0983 | |||
| 1b3a2f2e5b | |||
| 653d974da7 | |||
| 332854bbcb | |||
| 911447304c | |||
| 2be1062f1e | |||
| 64be7b04e8 | |||
| 925c463fe5 | |||
| e1bd6b3355 | |||
| fd90f79ede | |||
| 5d303ce6f2 | |||
| d98221ba7f | |||
| 29026a379b | |||
| 0706482232 | |||
| 2e273e8fe2 | |||
| a5d59de39e | |||
| 71808fafd1 | |||
| 82ebfc7908 | |||
| f0996686e9 | |||
| e422fb5e4d | |||
| ceac10b098 | |||
| 365fabf9a0 | |||
| 21e1375760 | |||
| d7656314d6 | |||
| 5368bcf6bd | |||
| adf5970245 | |||
| aca2f54cb7 | |||
| 6313db698a | |||
| c9811beae2 | |||
| 97533cba2e | |||
| e92459bf1e | |||
| cc74cae393 | |||
| 58def055d0 | |||
| 41f5261fe5 | |||
| ce1a54eb3b | |||
| 25285488f8 | |||
| befdcef1f0 | |||
| fa18e6c012 | |||
| 4194bce4a3 | |||
| da08e5015f | |||
| 9b7472844d | |||
| eace78fa04 | |||
| e81b2a7839 | |||
| cdcfc81aca | |||
| 816b0f9e3d | |||
| 6fb073672d | |||
| 9203bd8e67 | |||
| 3252d9ec31 | |||
| 45fdf35e20 | |||
| 53841d7bf0 | |||
| d2c779eb0f | |||
| cbe51707a5 | |||
| 3d7a6d8e9d | |||
| 72cdb5edc4 | |||
| 1f11a8dde0 | |||
| af427aa32d | |||
| 212f8539c3 | |||
| 9a5e6e58be | |||
| c31700ae5f | |||
| cfb96859b3 | |||
| 9bc641d15e | |||
| a79075908e | |||
| 7f9091f205 | |||
| 9f6887b048 | |||
| 982906cbad | |||
| 90514a73bb | |||
| 2d63c5db50 | |||
| 768036a223 | |||
| 2d8a41b18b | |||
| 67542a54e8 | |||
| f1d89c413b | |||
| 90593adea5 | |||
| 235ab90c3d | |||
| ef7f112227 | |||
| db70faa4c7 | |||
| 1d87b5ea43 | |||
| d3f10941fd | |||
| 6dc556b704 | |||
| b6d1e0d868 | |||
| f2c5243905 | |||
| 803350ae6e | |||
| b225423ce0 | |||
| e36e3d54f2 | |||
| be7790bbdf | |||
| feb4fcdac6 | |||
| a6de3f0ff3 | |||
| 1c80a74885 | |||
| 4c6f27e978 | |||
| 367d7bdb4c | |||
| d998a2064e | |||
| ba96397f53 | |||
| 244988e212 | |||
| b1319fb88c | |||
| cff2ebba49 | |||
| 25c7ff173f | |||
| a6dbf8433c | |||
| 73f8d4f0d7 | |||
| 61c7605b54 | |||
| 8a70ce4cff | |||
| 4f85119dfd | |||
| 94170dec3b | |||
| bf05dac668 | |||
| 215c8ed6b2 | |||
| 2483da251a | |||
| bf15d8aff1 | |||
| 4b0f677774 | |||
| 886429b0ed | |||
| f65157a14f | |||
| 3d67bb81e9 | |||
| d30e9713f4 | |||
| f2babbf637 | |||
| fc609470c4 | |||
| 07f82eae54 | |||
| 13ebfcc5b9 | |||
| 2a04d17e1f | |||
| 002ae4bc0d | |||
| 047cb00417 | |||
| 2c014f725a | |||
| a69e04f03a | |||
| c5488df8ca | |||
| 1caec27f72 | |||
| e14c954e12 | |||
| 8e61f5d631 | |||
| 08db565a72 | |||
| 6d0884254e | |||
| 665ccf88b7 | |||
| 1ff7b861ca | |||
| c531a9d986 | |||
| 0bf500fa95 | |||
| ceabfba767 | |||
| 27e8f5c59a | |||
| fff1c0a6f2 | |||
| 7d11123c0a | |||
| 8da495ac00 | |||
| 69ec68ee31 | |||
| a6ea8444c9 | |||
| 4c1ee3d6ea | |||
| 8e38db96a5 | |||
| f154fcc9c5 | |||
| 4906fe9cf0 | |||
| 3f6fda0c3e | |||
| 6989f8366b | |||
| 9e61c6a490 | |||
| d32157212c | |||
| 926459893b | |||
| a0c7592110 | |||
| 021efa382c | |||
| 308715b4ca | |||
| 5fe2bab381 | |||
| eda348226d | |||
| 3c492bc202 | |||
| d74783c231 | |||
| 3af8ae5faf | |||
| 79624c1d1c | |||
| c063dadb74 | |||
| d361e24bee | |||
| bff38dceb5 | |||
| 85ac0c6036 | |||
| af7194d078 | |||
| 0ecbcfdd86 | |||
| 40152af775 | |||
| 6d4c4d214f | |||
| aba51ab784 | |||
| 6d5e701ef0 | |||
| 60c31787d7 | |||
| 3772b5ac78 | |||
| d7a65f3e7a | |||
| 5cb59d8a76 | |||
| 2d350b8168 | |||
| b027d69365 | |||
| c8eae9d37a | |||
| 8e6eb4f6d8 | |||
| e81380314c | |||
| 9ce3f790f9 | |||
| 04645e8329 | |||
| e47b6cf166 | |||
| 09aaa807a6 | |||
| fd9a748553 | |||
| 1f5bcc73b5 | |||
| 35c8e35674 | |||
| 9d1398f2d9 | |||
| 764f03a3c5 | |||
| b999c86e23 | |||
| e79f0cc847 | |||
| ba3c5bdf33 | |||
| 6976311e18 | |||
| 3dd303bd5b | |||
| 1679a184ad | |||
| 64862db846 | |||
| c65d4822b4 | |||
| b4be736555 | |||
| 4e0bc485a1 | |||
| b590a318a2 | |||
| 55dcdc7b52 | |||
| 6952ec2c87 | |||
| b3a6f4e0ac | |||
| a538fa879f | |||
| 3b15224453 | |||
| d8006e15ac | |||
| f89d93b905 | |||
| 2dbf550a70 | |||
| 0ac7f4b24b | |||
| 8d1e41693a | |||
| 4adc773c77 | |||
| b5cdb0c855 | |||
| 7652d8e131 | |||
| ce89925176 | |||
| 33c108b739 | |||
| 382ce8daff | |||
| 3d53d4c086 | |||
| 56a5b58849 | |||
| 74c2076262 | |||
| a84e601da8 | |||
| 9a3eab5ac4 |
+9
-18
@@ -1,28 +1,19 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [
|
||||
[
|
||||
"@trigger.dev/*",
|
||||
"trigger.dev"
|
||||
]
|
||||
"changelog": [
|
||||
"@remix-run/changelog-github",
|
||||
{
|
||||
"repo": "triggerdotdev/trigger.dev"
|
||||
}
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [["@trigger.dev/*", "trigger.dev"]],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [
|
||||
"webapp",
|
||||
"emails",
|
||||
"proxy",
|
||||
"@trigger.dev/database",
|
||||
"coordinator",
|
||||
"docker-provider",
|
||||
"kubernetes-provider",
|
||||
"@trigger.dev/otlp-importer"
|
||||
],
|
||||
"ignore": ["webapp", "proxy", "coordinator", "docker-provider", "kubernetes-provider"],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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=
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [triggerdotdev]
|
||||
@@ -0,0 +1,84 @@
|
||||
name: "#️⃣ Get image tag (action)"
|
||||
|
||||
description: This action gets the image tag from the commit ref or input (if provided)
|
||||
|
||||
outputs:
|
||||
tag:
|
||||
description: The image tag
|
||||
value: ${{ steps.get_tag.outputs.tag }}
|
||||
is_semver:
|
||||
description: Whether the tag is a semantic version
|
||||
value: ${{ steps.check_semver.outputs.is_semver }}
|
||||
|
||||
inputs:
|
||||
tag:
|
||||
description: The image tag. If this is set it will return the tag as is.
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: "#️⃣ Get image tag (step)"
|
||||
id: get_tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -n "${{ inputs.tag }}" ]]; then
|
||||
tag="${{ inputs.tag }}"
|
||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
if [[ "${{ github.ref_name }}" == infra-*-* ]]; then
|
||||
env=$(echo ${{ github.ref_name }} | cut -d- -f2)
|
||||
sha=$(echo ${{ github.sha }} | head -c7)
|
||||
ts=$(date +%s)
|
||||
tag=${env}-${sha}-${ts}
|
||||
elif [[ "${{ github.ref_name }}" == v.docker.* ]]; then
|
||||
version="${GITHUB_REF_NAME#v.docker.}"
|
||||
tag="v${version}"
|
||||
elif [[ "${{ github.ref_name }}" == build-* ]]; then
|
||||
tag="${GITHUB_REF_NAME#build-}"
|
||||
else
|
||||
echo "Invalid git tag: ${{ github.ref_name }}"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
tag="main"
|
||||
else
|
||||
echo "Invalid git ref: ${{ github.ref }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔍 Check for validity
|
||||
id: check_validity
|
||||
shell: bash
|
||||
env:
|
||||
tag: ${{ steps.get_tag.outputs.tag }}
|
||||
run: |
|
||||
if [[ "${tag}" =~ ^[a-z0-9]+([._-][a-z0-9]+)*$ ]]; then
|
||||
echo "Tag is valid: ${tag}"
|
||||
else
|
||||
echo "Tag is not valid: ${tag}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 🆚 Check for semver
|
||||
id: check_semver
|
||||
shell: bash
|
||||
env:
|
||||
tag: ${{ steps.get_tag.outputs.tag }}
|
||||
# Will match most semver formats except build metadata, i.e. v1.2.3+build.1
|
||||
# Valid matches:
|
||||
# v1.2.3
|
||||
# v1.2.3-alpha
|
||||
# v1.2.3-alpha.1
|
||||
# v1.2.3-rc.1
|
||||
# v1.2.3-beta-1
|
||||
run: |
|
||||
if [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
|
||||
echo "Tag is a semantic version: ${tag}"
|
||||
is_semver=true
|
||||
else
|
||||
echo "Tag is not a semantic version: ${tag}"
|
||||
is_semver=false
|
||||
fi
|
||||
echo "is_semver=${is_semver}" >> "$GITHUB_OUTPUT"
|
||||
@@ -0,0 +1 @@
|
||||
This is the repo for Trigger.dev, a background jobs platform written in TypeScript. Our webapp at apps/webapp is a Remix 2.1 app that uses Node.js v20. Our SDK is an isomorphic TypeScript SDK at packages/trigger-sdk. Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code. Our tests are all vitest. We use prisma in internal-packages/database for our database interactions using PostgreSQL. For TypeScript, we usually use types over interfaces. We use zod a lot in packages/core and in the webapp. Avoid enums. Use strict mode. No default exports, use function declarations.
|
||||
@@ -0,0 +1,70 @@
|
||||
# GitHub Action Tests
|
||||
|
||||
This directory contains necessary files to allow local testing of GitHub Actions workflows, composite actions, etc. You will need to install [act](https://github.com/nektos/act) to perform tests.
|
||||
|
||||
## Workflow tests
|
||||
|
||||
Trigger specific workflow files by specifying their full path:
|
||||
|
||||
```
|
||||
act -W .github/workflow/release.yml
|
||||
```
|
||||
|
||||
You will likely need to override any custom runners we use, e.g. buildjet. For example:
|
||||
|
||||
```
|
||||
override=catthehacker/ubuntu:act-latest
|
||||
|
||||
act -W .github/workflow/release.yml \
|
||||
-P buildjet-8vcpu-ubuntu-2204=$override
|
||||
|
||||
# override multiple images at the same time
|
||||
act -W .github/workflow/release.yml \
|
||||
-P buildjet-8vcpu-ubuntu-2204=$override \
|
||||
-P buildjet-16vcpu-ubuntu-2204=$override
|
||||
```
|
||||
|
||||
Trigger with specific event payloads to test pushing to branches or tags:
|
||||
|
||||
```
|
||||
override=catthehacker/ubuntu:act-latest
|
||||
|
||||
# simulate push to main
|
||||
act -W .github/workflow/publish.yml \
|
||||
-P buildjet-8vcpu-ubuntu-2204=$override \
|
||||
-P buildjet-16vcpu-ubuntu-2204=$override \
|
||||
-e .github/events/push-tag-main.json
|
||||
|
||||
# simulate a `build-` prefixed tag
|
||||
act -W .github/workflow/publish.yml \
|
||||
-P buildjet-8vcpu-ubuntu-2204=$override \
|
||||
-P buildjet-16vcpu-ubuntu-2204=$override \
|
||||
-e .github/events/push-tag-buld.json
|
||||
```
|
||||
|
||||
By default, `act` will send a push event. To trigger a different event:
|
||||
|
||||
```
|
||||
# basic syntax
|
||||
act <EVENT> ...
|
||||
|
||||
# simulate a pull request
|
||||
act pull_request
|
||||
|
||||
# only trigger a specific workflow
|
||||
act pull_request -W .github/workflow/pr_checks.yml
|
||||
```
|
||||
|
||||
## Composite action tests
|
||||
|
||||
The composite (custom) action tests can be run by triggering the `test-actions` workflow:
|
||||
|
||||
```
|
||||
act -W .github/test/test-actions.yml
|
||||
```
|
||||
|
||||
## Helpful flags
|
||||
|
||||
- `--pull=false` - perform fully offline tests if all images are already present
|
||||
- `-j <job_name>` - run the specified job only
|
||||
- `-l push` - list all workflows with push triggers
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/heads/main"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/tags/build-buildtag"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/tags/v.docker.nonsemver"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/tags/v.docker.1.2.3"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/tags/infra-prod-anything"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/tags/infra-test-anything"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/tags/1.2.3"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ref": "refs/tags/standard-tag"
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
name: Test Actions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
get-image-tag-none:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log current ref
|
||||
run: |
|
||||
echo "ref: ${{ github.ref }}"
|
||||
echo "ref_type: ${{ github.ref_type }}"
|
||||
echo "ref_name: ${{ github.ref_name }}"
|
||||
|
||||
- name: Run without input tag
|
||||
id: get_tag
|
||||
# this step may fail depending on the current ref
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/get-image-tag
|
||||
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "${{ toJson(steps.get_tag) }}"
|
||||
|
||||
get-image-tag-null:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log current ref
|
||||
run: |
|
||||
echo "ref: ${{ github.ref }}"
|
||||
echo "ref_type: ${{ github.ref_type }}"
|
||||
echo "ref_name: ${{ github.ref_name }}"
|
||||
|
||||
- name: Run without input tag
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
# this step may fail depending on the current ref
|
||||
continue-on-error: true
|
||||
with:
|
||||
# this should behave exactly as when no tag is provided
|
||||
tag: null
|
||||
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "${{ toJson(steps.get_tag) }}"
|
||||
|
||||
get-image-tag-override:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run with tag override
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
with:
|
||||
tag: "abc-123"
|
||||
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "${{ toJson(steps.get_tag) }}"
|
||||
|
||||
get-image-tag-invalid-string:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run with invalid string
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
# this step is expected to fail
|
||||
continue-on-error: true
|
||||
with:
|
||||
# does not end with alphanumeric character
|
||||
tag: "abc-123-"
|
||||
|
||||
- name: Fail job if previous step did not fail
|
||||
if: steps.get_tag.outcome != 'failure'
|
||||
run: exit 1
|
||||
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "${{ toJson(steps.get_tag) }}"
|
||||
|
||||
get-image-tag-prerelease:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run with prerelease semver
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
with:
|
||||
tag: "v1.2.3-beta.4"
|
||||
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "${{ toJson(steps.get_tag) }}"
|
||||
|
||||
get-image-tag-semver:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run with basic semver
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
with:
|
||||
tag: "v1.2.3"
|
||||
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "${{ toJson(steps.get_tag) }}"
|
||||
|
||||
get-image-tag-invalid-semver:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run with invalid semver
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
# this step is expected to fail
|
||||
continue-on-error: true
|
||||
with:
|
||||
tag: "v1.2.3-"
|
||||
|
||||
- name: Fail job if previous step did not fail
|
||||
if: steps.get_tag.outcome != 'failure'
|
||||
run: exit 1
|
||||
|
||||
- name: Verify output
|
||||
run: |
|
||||
echo "${{ toJson(steps.get_tag) }}"
|
||||
@@ -0,0 +1,42 @@
|
||||
name: 📚 Docs Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- "docs/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-broken-links:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./docs
|
||||
steps:
|
||||
- name: 📥 Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📦 Cache npm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
key: |
|
||||
${{ runner.os }}-mintlify
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mintlify
|
||||
|
||||
- name: 🔗 Check for broken links
|
||||
run: npx mintlify@4.0.222 broken-links
|
||||
@@ -1,4 +1,5 @@
|
||||
name: "E2E"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -7,6 +8,7 @@ on:
|
||||
default: webapp
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
cli-v3:
|
||||
name: "🧪 CLI v3 tests (${{ matrix.os }} - ${{ matrix.package-manager }})"
|
||||
@@ -19,7 +21,7 @@ jobs:
|
||||
package-manager: ["npm", "pnpm"]
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -29,7 +31,7 @@ jobs:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
|
||||
@@ -49,5 +51,3 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
LOG=debug PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ name: 🤖 PR Checks
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -30,12 +32,11 @@ jobs:
|
||||
preview-release:
|
||||
name: Preview Release
|
||||
needs: [typecheck, units, e2e]
|
||||
if: |
|
||||
github.repository == 'triggerdotdev/trigger.dev'
|
||||
if: github.repository == 'triggerdotdev/trigger.dev'
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -45,7 +46,7 @@ jobs:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
@@ -56,5 +57,5 @@ jobs:
|
||||
- name: 🏗️ Build
|
||||
run: pnpm run build --filter "@trigger.dev/*" --filter "trigger.dev"
|
||||
|
||||
- name: ⚡ publish preview release
|
||||
- name: ⚡ Publish preview release
|
||||
run: npx pkg-pr-new publish --no-template $(ls -d ./packages/*)
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
name: "🐳 Publish Docker"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: 1
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
short_sha: ${{ steps.get_commit.outputs.sha_short }}
|
||||
steps:
|
||||
- name: Setup Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: 🆚 Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
IMAGE_TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
if [[ $IMAGE_TAG == v.docker.* ]]; then
|
||||
ORIGINAL_VERSION="${IMAGE_TAG#v.docker.}"
|
||||
IMAGE_TAG="v${ORIGINAL_VERSION}"
|
||||
elif [[ $IMAGE_TAG == build-* ]]; then
|
||||
IMAGE_TAG="${IMAGE_TAG#build-}"
|
||||
fi
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
# Handle main branch specifically
|
||||
IMAGE_TAG="main"
|
||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
||||
else
|
||||
echo "Invalid reference: ${GITHUB_REF}"
|
||||
exit 1
|
||||
fi
|
||||
echo "::set-output name=version::${IMAGE_TAG}"
|
||||
|
||||
- name: 🔢 Get the commit hash
|
||||
id: get_commit
|
||||
run: |
|
||||
echo ::set-output name=sha_short::$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
- name: 📛 Set the tags
|
||||
id: set_tags
|
||||
run: |
|
||||
ref_without_tag=ghcr.io/triggerdotdev/trigger.dev
|
||||
image_tags=$ref_without_tag:${{ steps.get_version.outputs.version }}
|
||||
|
||||
# if it's a versioned tag, also tag it as v3
|
||||
if [[ "${{ github.ref_name }}" == v.docker.* ]]; then
|
||||
image_tags=$image_tags,$ref_without_tag:v3
|
||||
fi
|
||||
|
||||
echo "IMAGE_TAGS=${image_tags}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐳 Build image and push to GitHub Container Registry
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.set_tags.outputs.IMAGE_TAGS }}
|
||||
push: true
|
||||
@@ -1,121 +0,0 @@
|
||||
name: "🚢 Publish Infra Images"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
tags:
|
||||
- "infra-dev-*"
|
||||
- "infra-test-*"
|
||||
- "infra-prod-*"
|
||||
paths:
|
||||
- ".github/workflows/publish.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- "!packages/**/*.eslintrc"
|
||||
- "apps/**"
|
||||
- "!apps/**/*.md"
|
||||
- "!apps/**/*.eslintrc"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "!integrations/**/*.eslintrc"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
- "docker/Dockerfile"
|
||||
- "docker/scripts/**"
|
||||
- "tests/**"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
package: [coordinator, docker-provider, kubernetes-provider]
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate image reference
|
||||
id: prep
|
||||
run: |
|
||||
# set image repo
|
||||
if [[ "${{ matrix.package }}" == *-provider ]]; then
|
||||
provider_type=$(echo "${{ matrix.package }}" | cut -d- -f1)
|
||||
repository=provider/${provider_type}
|
||||
else
|
||||
repository="${{ matrix.package }}"
|
||||
fi
|
||||
echo "REPOSITORY=${repository}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# set image tag
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
if [[ "${{ github.ref_name }}" == infra-*-* ]]; then
|
||||
env=$(echo ${{ github.ref_name }} | cut -d- -f2)
|
||||
sha=$(echo ${{ github.sha }} | head -c7)
|
||||
ts=$(date +%s)
|
||||
image_tag=${env}-${sha}-${ts}
|
||||
elif [[ "${{ github.ref_name }}" == v.docker.* ]]; then
|
||||
version="${GITHUB_REF_NAME#v.docker.}"
|
||||
image_tag="v${version}"
|
||||
elif [[ "${{ github.ref_name }}" == build-* ]]; then
|
||||
image_tag="${GITHUB_REF_NAME#build-}"
|
||||
else
|
||||
echo "Invalid tag: ${{ github.ref_name }}"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
image_tag="main"
|
||||
else
|
||||
echo "Invalid reference: ${{ github.ref }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "IMAGE_TAG=${image_tag}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# ..to avoid rate limits when pulling images
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 🚢 Build Container Image
|
||||
run: |
|
||||
docker build -t infra_image -f ./apps/${{ matrix.package }}/Containerfile .
|
||||
|
||||
# ..to push image
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐙 Push to GitHub Container Registry
|
||||
run: |
|
||||
docker tag infra_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: ${{ steps.prep.outputs.REPOSITORY }}
|
||||
IMAGE_TAG: ${{ steps.prep.outputs.IMAGE_TAG }}
|
||||
|
||||
- name: 🐙 Push 'v3' tag to GitHub Container Registry
|
||||
if: startsWith(github.ref_name, 'v.docker.')
|
||||
run: |
|
||||
docker tag infra_image $REGISTRY/$REPOSITORY:v3
|
||||
docker push $REGISTRY/$REPOSITORY:v3
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: ${{ steps.prep.outputs.REPOSITORY }}
|
||||
@@ -0,0 +1,66 @@
|
||||
name: "🐳 Publish Webapp"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: The image tag to publish
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: 1
|
||||
outputs:
|
||||
version: ${{ steps.get_tag.outputs.tag }}
|
||||
short_sha: ${{ steps.get_commit.outputs.sha_short }}
|
||||
steps:
|
||||
- name: 🏭 Setup Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: "#️⃣ Get the image tag"
|
||||
id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
with:
|
||||
tag: ${{ inputs.image_tag }}
|
||||
|
||||
- name: 🔢 Get the commit hash
|
||||
id: get_commit
|
||||
run: |
|
||||
echo "sha_short=$(echo ${{ github.sha }} | cut -c1-7)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 📛 Set the tags
|
||||
id: set_tags
|
||||
run: |
|
||||
ref_without_tag=ghcr.io/triggerdotdev/trigger.dev
|
||||
image_tags=$ref_without_tag:${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
# if tag is a semver, also tag it as v3
|
||||
if [[ "${{ steps.get_tag.outputs.is_semver }}" == true ]]; then
|
||||
image_tags=$image_tags,$ref_without_tag:v3
|
||||
fi
|
||||
|
||||
echo "image_tags=${image_tags}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐳 Build image and push to GitHub Container Registry
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.set_tags.outputs.image_tags }}
|
||||
push: true
|
||||
@@ -0,0 +1,87 @@
|
||||
name: "⚒️ Publish Worker"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: The image tag to publish
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
push:
|
||||
tags:
|
||||
- "infra-dev-*"
|
||||
- "infra-test-*"
|
||||
- "infra-prod-*"
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
package: [coordinator, docker-provider, kubernetes-provider]
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- name: ⬇️ Checkout git repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📦 Get image repo
|
||||
id: get_repository
|
||||
run: |
|
||||
if [[ "${{ matrix.package }}" == *-provider ]]; then
|
||||
provider_type=$(echo "${{ matrix.package }}" | cut -d- -f1)
|
||||
repo=provider/${provider_type}
|
||||
else
|
||||
repo="${{ matrix.package }}"
|
||||
fi
|
||||
echo "repo=${repo}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- id: get_tag
|
||||
uses: ./.github/actions/get-image-tag
|
||||
with:
|
||||
tag: ${{ inputs.image_tag }}
|
||||
|
||||
- name: 🐋 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# ..to avoid rate limits when pulling images
|
||||
- name: 🐳 Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 🚢 Build Container Image
|
||||
run: |
|
||||
docker build -t infra_image -f ./apps/${{ matrix.package }}/Containerfile .
|
||||
|
||||
# ..to push image
|
||||
- name: 🐙 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🐙 Push to GitHub Container Registry
|
||||
run: |
|
||||
docker tag infra_image "$REGISTRY/$REPOSITORY:$IMAGE_TAG"
|
||||
docker push "$REGISTRY/$REPOSITORY:$IMAGE_TAG"
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: ${{ steps.get_repository.outputs.repo }}
|
||||
IMAGE_TAG: ${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
- name: 🐙 Push 'v3' tag to GitHub Container Registry
|
||||
if: steps.get_tag.outputs.is_semver == 'true'
|
||||
run: |
|
||||
docker tag infra_image "$REGISTRY/$REPOSITORY:v3"
|
||||
docker push "$REGISTRY/$REPOSITORY:v3"
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: ${{ steps.get_repository.outputs.repo }}
|
||||
@@ -1,6 +1,12 @@
|
||||
name: 🚀 Publish Trigger.dev Docker
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: The image tag to publish
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -8,11 +14,13 @@ on:
|
||||
- "v.docker.*"
|
||||
- "build-*"
|
||||
paths:
|
||||
- ".github/actions/**/*.yml"
|
||||
- ".github/workflows/publish.yml"
|
||||
- ".github/workflows/typecheck.yml"
|
||||
- ".github/workflows/unit-tests.yml"
|
||||
- ".github/workflows/e2e.yml"
|
||||
- ".github/workflows/publish-docker.yml"
|
||||
- ".github/workflows/publish-webapp.yml"
|
||||
- ".github/workflows/publish-worker.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- "!packages/**/*.eslintrc"
|
||||
@@ -49,12 +57,16 @@ jobs:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
publish-webapp:
|
||||
needs: [typecheck, units]
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
uses: ./.github/workflows/publish-webapp.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_tag: ${{ inputs.image_tag }}
|
||||
|
||||
publish-infra:
|
||||
publish-worker:
|
||||
needs: [typecheck, units]
|
||||
uses: ./.github/workflows/publish-infra.yml
|
||||
uses: ./.github/workflows/publish-worker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_tag: ${{ inputs.image_tag }}
|
||||
|
||||
@@ -5,26 +5,27 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
- "**.mdx"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: 🦋 Changesets Release
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
if: |
|
||||
github.repository == 'triggerdotdev/trigger.dev'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'triggerdotdev/trigger.dev'
|
||||
outputs:
|
||||
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
published: ${{ steps.changesets.outputs.published }}
|
||||
published_packages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
published_package_version: ${{ steps.get_version.outputs.package_version }}
|
||||
steps:
|
||||
- name: 🛑 Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
@@ -62,6 +63,7 @@ jobs:
|
||||
# PR is merged, the workflow will run again and this action will build +
|
||||
# publish to npm.
|
||||
- name: 🚀 PR / Publish
|
||||
if: ${{ !env.ACT }}
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
@@ -73,3 +75,27 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
# - name: 🚀 PR / Publish (mock)
|
||||
# if: ${{ env.ACT }}
|
||||
# id: changesets
|
||||
# run: |
|
||||
# echo "published=true" >> "$GITHUB_OUTPUT"
|
||||
# echo "publishedPackages=[{\"name\": \"@xx/xx\", \"version\": \"1.2.0\"}, {\"name\": \"@xx/xy\", \"version\": \"0.8.9\"}]" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 📦 Get package version
|
||||
if: steps.changesets.outputs.published == 'true'
|
||||
id: get_version
|
||||
run: |
|
||||
package_version=$(echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[0].version')
|
||||
echo "package_version=${package_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
publish:
|
||||
needs: release
|
||||
uses: ./.github/workflows/publish.yml
|
||||
secrets: inherit
|
||||
# if: needs.release.outputs.published == 'true'
|
||||
# disable automatic publishing for now
|
||||
if: false
|
||||
with:
|
||||
image_tag: v${{ needs.release.outputs.published_package_version }}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
name: "ʦ TypeScript"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -17,7 +19,7 @@ jobs:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
@@ -30,6 +32,6 @@ jobs:
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
|
||||
- name: 🔎 Check exports
|
||||
run: pnpm run check-exports
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
name: "🧪 Unit Tests"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests"
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -17,7 +19,7 @@ jobs:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
@@ -25,6 +27,20 @@ jobs:
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
pnpm run test
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🧪 Run Webapp Unit Tests
|
||||
run: pnpm run test --filter webapp
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
SESSION_SECRET: "secret"
|
||||
MAGIC_LINK_SECRET: "secret"
|
||||
ENCRYPTION_KEY: "secret"
|
||||
|
||||
- name: 🧪 Run Package Unit Tests
|
||||
run: pnpm run test --filter "@trigger.dev/*"
|
||||
|
||||
- name: 🧪 Run Internal Unit Tests
|
||||
run: pnpm run test --filter "@internal/*"
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
[submodule "packages/otlp-importer/protos"]
|
||||
path = packages/otlp-importer/protos
|
||||
[submodule "internal-packages/otlp-importer/protos"]
|
||||
path = internal-packages/otlp-importer/protos
|
||||
url = https://github.com/open-telemetry/opentelemetry-proto.git
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
link-workspace-packages=false
|
||||
public-hoist-pattern[]=*prisma*
|
||||
public-hoist-pattern[]=*prisma*
|
||||
prefer-workspace-packages=true
|
||||
Vendored
+2
-6
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"denoland.vscode-deno"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
|
||||
]
|
||||
"recommendations": ["bierner.comment-tagged-templates"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
|
||||
Vendored
+31
-6
@@ -13,6 +13,15 @@
|
||||
"cwd": "${workspaceFolder}",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug realtimeStreams.test.ts",
|
||||
"command": "pnpm run test -t RealtimeStreams",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"cwd": "${workspaceFolder}/apps/webapp",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
@@ -24,7 +33,7 @@
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 init CLI",
|
||||
"command": "pnpm exec triggerdev init",
|
||||
"command": "pnpm exec trigger init",
|
||||
"cwd": "${workspaceFolder}/references/init-shell",
|
||||
"sourceMaps": true
|
||||
},
|
||||
@@ -32,15 +41,31 @@
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 Dev CLI",
|
||||
"command": "pnpm exec triggerdev dev",
|
||||
"command": "pnpm exec trigger dev",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug Dev Next.js Realtime",
|
||||
"command": "pnpm exec trigger dev",
|
||||
"cwd": "${workspaceFolder}/references/nextjs-realtime",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug prisma-catalog deploy CLI",
|
||||
"command": "pnpm exec trigger deploy --self-hosted --load-image",
|
||||
"cwd": "${workspaceFolder}/references/prisma-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 Deploy CLI",
|
||||
"command": "pnpm exec triggerdev deploy --self-hosted --load-image",
|
||||
"command": "pnpm exec trigger deploy --self-hosted --load-image",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
@@ -48,7 +73,7 @@
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 list-profiles CLI",
|
||||
"command": "pnpm exec triggerdev list-profiles --log-level debug",
|
||||
"command": "pnpm exec trigger list-profiles --log-level debug",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
@@ -56,7 +81,7 @@
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug V3 update CLI",
|
||||
"command": "pnpm exec triggerdev update",
|
||||
"command": "pnpm exec trigger update",
|
||||
"cwd": "${workspaceFolder}/references/v3-catalog",
|
||||
"sourceMaps": true
|
||||
},
|
||||
@@ -88,7 +113,7 @@
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "debug v3 hello-world dev",
|
||||
"command": "pnpm exec triggerdev dev",
|
||||
"command": "pnpm exec trigger dev",
|
||||
"cwd": "${workspaceFolder}/references/hello-world",
|
||||
"sourceMaps": true
|
||||
}
|
||||
|
||||
+7
-7
@@ -114,7 +114,7 @@ pnpm i
|
||||
```sh
|
||||
cd references/v3-catalog
|
||||
cp .env.example .env
|
||||
pnpm exec triggerdev login -a http://localhost:3030
|
||||
pnpm exec trigger login -a http://localhost:3030
|
||||
```
|
||||
|
||||
This will open a new browser window and authorize the CLI against your local user account.
|
||||
@@ -123,10 +123,10 @@ You can optionally pass a `--profile` flag to the `login` command, which will al
|
||||
|
||||
```sh
|
||||
cd references/v3-catalog
|
||||
pnpm exec triggerdev login -a http://localhost:3030 --profile local
|
||||
pnpm exec trigger login -a http://localhost:3030 --profile local
|
||||
# later when you run the dev or deploy command:
|
||||
pnpm exec triggerdev dev --profile local
|
||||
pnpm exec triggerdev deploy --profile local
|
||||
pnpm exec trigger dev --profile local
|
||||
pnpm exec trigger deploy --profile local
|
||||
```
|
||||
|
||||
### Running
|
||||
@@ -155,14 +155,14 @@ Note: You do not need to do the same for `@trigger.dev/sdk`, just core.
|
||||
|
||||
```sh
|
||||
# in <root>/references/v3-catalog
|
||||
pnpm exec triggerdev dev
|
||||
pnpm exec trigger dev
|
||||
```
|
||||
|
||||
If you want additional debug logging, you can use the `--log-level debug` flag:
|
||||
|
||||
```sh
|
||||
# in <root>/references/v3-catalog
|
||||
pnpm exec triggerdev dev --log-level debug
|
||||
pnpm exec trigger dev --log-level debug
|
||||
```
|
||||
|
||||
5. If you make any changes in the CLI/Core/SDK, you'll need to `CTRL+C` to exit the `dev` command and restart it to pickup changes. Any changes to the files inside of the `v3-catalog/src/trigger` dir will automatically be rebuilt by the `dev` command.
|
||||
@@ -223,7 +223,7 @@ pnpm run db:studio
|
||||
|
||||
## Adding and running migrations
|
||||
|
||||
1. Modify packages/database/prisma/schema.prisma file
|
||||
1. Modify internal-packages/database/prisma/schema.prisma file
|
||||
2. Change directory to the packages/database folder
|
||||
|
||||
```sh
|
||||
|
||||
@@ -15,13 +15,20 @@
|
||||
|
||||
## About Trigger.dev
|
||||
|
||||
Trigger.dev is an open source platform and SDK which allows you to create long-running background jobs with no timeouts. Write normal async code, deploy, and never hit a timeout.
|
||||
Trigger.dev is an open source platform and SDK which allows you to create long-running background jobs. Write normal async code, deploy, and never hit a timeout.
|
||||
|
||||
#### Features:
|
||||
### Key features:
|
||||
|
||||
- JavaScript and TypeScript SDK
|
||||
- Write reliable code by default
|
||||
- No timeouts
|
||||
- Retries (with exponential backoff)
|
||||
- Queues and concurrency controls
|
||||
- Schedules and crons
|
||||
- Full Observability; logs, live trace views, advanced filtering
|
||||
- Custom alerts, get notified by email, Slack or webhooks
|
||||
- No infrastructure to manage
|
||||
- Elastic (scaling)
|
||||
- Works with your existing tech stack
|
||||
|
||||
## In your codebase
|
||||
@@ -45,7 +52,7 @@ export const helloWorld = task({
|
||||
|
||||
## Deployment
|
||||
|
||||
Use our SDK to write tasks in your codebase. There's no infrastructure to manage, your tasks automatically scale and connect to our cloud. Or you can always [self-host](https://trigger.dev/docs/v3/open-source-self-hosting#overview).
|
||||
Use our SDK to write tasks in your codebase. There's no infrastructure to manage, your tasks automatically scale and connect to our cloud. Or you can always self-host.
|
||||
|
||||
## Environments
|
||||
|
||||
@@ -59,11 +66,19 @@ View every task in every run so you can tell exactly what happened. We provide a
|
||||
|
||||
# Getting started
|
||||
|
||||
Visit our docs [here](https://trigger.dev/docs/v3/introduction) for a full guide on how to get started with Trigger.dev.
|
||||
The quickest way to get started is to create an account and project in our [web app](https://cloud.trigger.dev), and follow the instructions in the onboarding. Build and deploy your first task in minutes.
|
||||
|
||||
## Self-host
|
||||
### Useful links:
|
||||
|
||||
If you prefer to self-host, you can follow our [self-hosting guide](https://trigger.dev/docs/v3/open-source-self-hosting#overview).
|
||||
- [Quick start](https://trigger.dev/docs/quick-start) - get up and running in minutes
|
||||
- [How it works](https://trigger.dev/docs/v3/how-it-works) - understand how Trigger.dev works under the hood
|
||||
- [Guides and examples](https://trigger.dev/docs/guides/introduction) - walk-through guides and code examples for popular frameworks and use cases
|
||||
|
||||
## Self-hosting
|
||||
|
||||
If you prefer to self-host Trigger.dev, you can follow our [self-hosting guide](https://trigger.dev/docs/v3/open-source-self-hosting#overview).
|
||||
|
||||
We also have a dedicated self-hosting channel in our [Discord server](https://trigger.dev/discord) for support.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
## Guide on releasing a new version
|
||||
|
||||
1. Merge in the changeset PR into main, making sure to cancel both the release and publish github actions from that merge.
|
||||
2. Pull the changes locally into main
|
||||
3. Run `pnpm i` which will update the pnpm lock file with the new versions
|
||||
4. create a commit with "Release 3.x.x" and push. This will build and release the packages
|
||||
5. Create a git tag on that release commit with v.docker.3.x.x and push the tag to origin. This will publish the `v3.x.x` docker image to GitHub Container Registry.
|
||||
6. Once the image is built and pushed, create a new GitHub release and select the tag you just created, along with the previous tag that was released.
|
||||
7. This will generate some release notes. Edit out the package changes and leave only the server changes.
|
||||
8. Name the release `@trigger.dev/docker@3.x.x`
|
||||
9. Include the package link (e.g. https://github.com/triggerdotdev/trigger.dev/pkgs/container/trigger.dev/278459584?tag=v3.x.x)
|
||||
10. Once the packages have been published, head over to the [v2-legacy repo](https://github.com/triggerdotdev/v2-legacy.trigger.dev) and follow the instructions in the README for creating a matching release.
|
||||
11. Before deploying to cloud, compare the differences in the previously created release and double check to see if there are any migrations with indexes created concurrently, and make sure to run those before deploying.
|
||||
@@ -17,10 +17,10 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"execa": "^8.0.1",
|
||||
"nanoid": "^5.0.6",
|
||||
"prom-client": "^15.1.0",
|
||||
"socket.io": "4.7.4"
|
||||
"socket.io": "4.7.4",
|
||||
"tinyexec": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Execa$ } from "execa";
|
||||
import { setTimeout as timeout } from "node:timers/promises";
|
||||
|
||||
class ChaosMonkeyError extends Error {
|
||||
@@ -35,11 +34,9 @@ export class ChaosMonkey {
|
||||
}
|
||||
|
||||
async call({
|
||||
$,
|
||||
throwErrors = !this.disableErrors,
|
||||
addDelays = !this.disableDelays,
|
||||
}: {
|
||||
$?: Execa$<string>;
|
||||
throwErrors?: boolean;
|
||||
addDelays?: boolean;
|
||||
} = {}) {
|
||||
@@ -60,11 +57,7 @@ export class ChaosMonkey {
|
||||
chaosEvents.push(async () => {
|
||||
console.log("🍌 Chaos monkey: Add delay");
|
||||
|
||||
if ($) {
|
||||
await $`sleep ${this.delayInSeconds}`;
|
||||
} else {
|
||||
await timeout(this.delayInSeconds * 1000);
|
||||
}
|
||||
await timeout(this.delayInSeconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,11 +65,7 @@ export class ChaosMonkey {
|
||||
chaosEvents.push(async () => {
|
||||
console.log("🍌 Chaos monkey: Throw error");
|
||||
|
||||
if ($) {
|
||||
await $`false`;
|
||||
} else {
|
||||
throw new ChaosMonkey.Error("🍌 Chaos monkey: Throw error");
|
||||
}
|
||||
throw new ChaosMonkey.Error("🍌 Chaos monkey: Throw error");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ExponentialBackoff } from "@trigger.dev/core/v3/apps";
|
||||
import { testDockerCheckpoint, isExecaChildProcess } from "@trigger.dev/core/v3/apps";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import { $ } from "execa";
|
||||
import { testDockerCheckpoint } from "@trigger.dev/core/v3/apps";
|
||||
import { nanoid } from "nanoid";
|
||||
import fs from "node:fs/promises";
|
||||
import { ChaosMonkey } from "./chaosMonkey";
|
||||
import { Buildah, Crictl, Exec } from "./exec";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
import { TempFileCleaner } from "./cleaner";
|
||||
import { numFromEnv, boolFromEnv } from "./util";
|
||||
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
|
||||
type CheckpointerInitializeReturn = {
|
||||
canCheckpoint: boolean;
|
||||
@@ -83,7 +86,7 @@ export class Checkpointer {
|
||||
#canCheckpoint = false;
|
||||
#dockerMode: boolean;
|
||||
|
||||
#logger = new SimpleLogger("[checkptr]");
|
||||
#logger = new SimpleStructuredLogger("checkpointer");
|
||||
#abortControllers = new Map<string, AbortController>();
|
||||
#failedCheckpoints = new Map<string, unknown>();
|
||||
#waitingForRetry = new Set<string>();
|
||||
@@ -93,7 +96,6 @@ export class Checkpointer {
|
||||
private registryTlsVerify: boolean;
|
||||
|
||||
private disableCheckpointSupport: boolean;
|
||||
private checkpointPath: string;
|
||||
|
||||
private simulateCheckpointFailure: boolean;
|
||||
private simulateCheckpointFailureSeconds: number;
|
||||
@@ -101,6 +103,7 @@ export class Checkpointer {
|
||||
private simulatePushFailureSeconds: number;
|
||||
|
||||
private chaosMonkey: ChaosMonkey;
|
||||
private tmpCleaner?: TempFileCleaner;
|
||||
|
||||
constructor(private opts: CheckpointerOptions) {
|
||||
this.#dockerMode = opts.dockerMode;
|
||||
@@ -110,7 +113,6 @@ export class Checkpointer {
|
||||
this.registryTlsVerify = opts.registryTlsVerify ?? true;
|
||||
|
||||
this.disableCheckpointSupport = opts.disableCheckpointSupport ?? false;
|
||||
this.checkpointPath = opts.checkpointPath ?? "/checkpoints";
|
||||
|
||||
this.simulateCheckpointFailure = opts.simulateCheckpointFailure ?? false;
|
||||
this.simulateCheckpointFailureSeconds = opts.simulateCheckpointFailureSeconds ?? 300;
|
||||
@@ -118,6 +120,7 @@ export class Checkpointer {
|
||||
this.simulatePushFailureSeconds = opts.simulatePushFailureSeconds ?? 300;
|
||||
|
||||
this.chaosMonkey = opts.chaosMonkey ?? new ChaosMonkey(!!process.env.CHAOS_MONKEY_ENABLED);
|
||||
this.tmpCleaner = this.#createTmpCleaner();
|
||||
}
|
||||
|
||||
async init(): Promise<CheckpointerInitializeReturn> {
|
||||
@@ -134,18 +137,17 @@ export class Checkpointer {
|
||||
return this.#getInitReturn(true);
|
||||
}
|
||||
|
||||
this.#logger.error(testCheckpoint.message, testCheckpoint.error ?? "");
|
||||
this.#logger.error(testCheckpoint.message, { error: testCheckpoint.error });
|
||||
return this.#getInitReturn(false);
|
||||
} else {
|
||||
try {
|
||||
await $`buildah login --get-login ${this.registryHost}`;
|
||||
} catch (error) {
|
||||
this.#logger.error(`No checkpoint support: Not logged in to registry ${this.registryHost}`);
|
||||
return this.#getInitReturn(false);
|
||||
}
|
||||
}
|
||||
|
||||
return this.#getInitReturn(true);
|
||||
const canLogin = await Buildah.canLogin(this.registryHost);
|
||||
|
||||
if (!canLogin) {
|
||||
this.#logger.error(`No checkpoint support: Not logged in to registry ${this.registryHost}`);
|
||||
}
|
||||
|
||||
return this.#getInitReturn(canLogin);
|
||||
}
|
||||
|
||||
#getInitReturn(canCheckpoint: boolean): CheckpointerInitializeReturn {
|
||||
@@ -183,7 +185,7 @@ export class Checkpointer {
|
||||
if (this.#dockerMode) {
|
||||
return basename;
|
||||
} else {
|
||||
return `${this.checkpointPath}/${basename}.tar`;
|
||||
return Crictl.getExportLocation(basename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +250,12 @@ export class Checkpointer {
|
||||
return false;
|
||||
}
|
||||
|
||||
controller.abort("cancelCheckpointing()");
|
||||
if (controller.signal.aborted) {
|
||||
this.#logger.debug("Controller already aborted", { runId });
|
||||
return false;
|
||||
}
|
||||
|
||||
controller.abort("cancelCheckpoint()");
|
||||
this.#abortControllers.delete(runId);
|
||||
|
||||
return true;
|
||||
@@ -284,7 +291,7 @@ export class Checkpointer {
|
||||
});
|
||||
|
||||
this.#waitingForRetry.add(runId);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay.milliseconds));
|
||||
await setTimeout(delay.milliseconds);
|
||||
|
||||
if (!this.#waitingForRetry.has(runId)) {
|
||||
this.#logger.log("Checkpoint canceled while waiting for retry", { runId });
|
||||
@@ -395,64 +402,63 @@ export class Checkpointer {
|
||||
const controller = new AbortController();
|
||||
this.#abortControllers.set(runId, controller);
|
||||
|
||||
const $$ = $({ signal: controller.signal });
|
||||
const onAbort = () => {
|
||||
this.#logger.error("Checkpoint aborted", { options });
|
||||
controller.signal.removeEventListener("abort", onAbort);
|
||||
};
|
||||
controller.signal.addEventListener("abort", onAbort);
|
||||
|
||||
const shortCode = nanoid(8);
|
||||
const imageRef = this.#getImageRef(projectRef, deploymentVersion, shortCode);
|
||||
const exportLocation = this.#getExportLocation(projectRef, deploymentVersion, shortCode);
|
||||
|
||||
const buildah = new Buildah({ id: `${runId}-${shortCode}`, abortSignal: controller.signal });
|
||||
const crictl = new Crictl({ id: `${runId}-${shortCode}`, abortSignal: controller.signal });
|
||||
|
||||
const cleanup = async () => {
|
||||
const metadata = {
|
||||
runId,
|
||||
exportLocation,
|
||||
imageRef,
|
||||
};
|
||||
|
||||
if (this.#dockerMode) {
|
||||
this.#logger.debug("Skipping cleanup in docker mode", metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await $`rm ${exportLocation}`;
|
||||
this.#logger.log("Deleted checkpoint archive", { exportLocation });
|
||||
this.#logger.log("Cleaning up", metadata);
|
||||
|
||||
await $`buildah rmi ${imageRef}`;
|
||||
this.#logger.log("Deleted checkpoint image", { imageRef });
|
||||
try {
|
||||
await buildah.cleanup();
|
||||
await crictl.cleanup();
|
||||
} catch (error) {
|
||||
this.#logger.error("Failure during checkpoint cleanup", { exportLocation, error });
|
||||
this.#logger.error("Error during cleanup", { ...metadata, error });
|
||||
}
|
||||
|
||||
// Ensure only the current controller is removed
|
||||
if (this.#abortControllers.get(runId) === controller) {
|
||||
this.#abortControllers.delete(runId);
|
||||
}
|
||||
controller.signal.removeEventListener("abort", onAbort);
|
||||
};
|
||||
|
||||
try {
|
||||
await this.chaosMonkey.call({ $: $$ });
|
||||
await this.chaosMonkey.call();
|
||||
|
||||
this.#logger.log("Checkpointing:", { options });
|
||||
|
||||
const containterName = this.#getRunContainerName(runId);
|
||||
const containterNameWithAttempt = this.#getRunContainerName(runId, attemptNumber);
|
||||
|
||||
// Create checkpoint (docker)
|
||||
if (this.#dockerMode) {
|
||||
try {
|
||||
if (this.opts.forceSimulate || !this.#canCheckpoint) {
|
||||
this.#logger.log("Simulating checkpoint");
|
||||
this.#logger.debug(await $$`docker pause ${containterNameWithAttempt}`);
|
||||
} else {
|
||||
if (this.simulateCheckpointFailure) {
|
||||
if (performance.now() < this.simulateCheckpointFailureSeconds * 1000) {
|
||||
this.#logger.error("Simulating checkpoint failure", { options });
|
||||
throw new Error("SIMULATE_CHECKPOINT_FAILURE");
|
||||
}
|
||||
}
|
||||
|
||||
if (leaveRunning) {
|
||||
this.#logger.debug(
|
||||
await $$`docker checkpoint create --leave-running ${containterNameWithAttempt} ${exportLocation}`
|
||||
);
|
||||
} else {
|
||||
this.#logger.debug(
|
||||
await $$`docker checkpoint create ${containterNameWithAttempt} ${exportLocation}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.#logger.error("Failed while creating docker checkpoint", { exportLocation });
|
||||
throw error;
|
||||
}
|
||||
await this.#createDockerCheckpoint(
|
||||
controller.signal,
|
||||
runId,
|
||||
exportLocation,
|
||||
leaveRunning,
|
||||
attemptNumber
|
||||
);
|
||||
|
||||
this.#logger.log("checkpoint created:", {
|
||||
runId,
|
||||
@@ -474,12 +480,7 @@ export class Checkpointer {
|
||||
return { success: false, reason: "SKIP_RETRYING" };
|
||||
}
|
||||
|
||||
const containerId = this.#logger.debug(
|
||||
// @ts-expect-error
|
||||
await $$`crictl ps`
|
||||
.pipeStdout($$({ stdin: "pipe" })`grep ${containterName}`)
|
||||
.pipeStdout($$({ stdin: "pipe" })`cut -f1 ${"-d "}`)
|
||||
);
|
||||
const containerId = await crictl.ps(containterName, true);
|
||||
|
||||
if (!containerId.stdout) {
|
||||
this.#logger.error("could not find container id", { options, containterName });
|
||||
@@ -496,7 +497,7 @@ export class Checkpointer {
|
||||
}
|
||||
|
||||
// Create checkpoint
|
||||
this.#logger.debug(await $$`crictl checkpoint --export=${exportLocation} ${containerId}`);
|
||||
await crictl.checkpoint(containerId.stdout, exportLocation);
|
||||
const postCheckpoint = performance.now();
|
||||
|
||||
// Print checkpoint size
|
||||
@@ -504,23 +505,20 @@ export class Checkpointer {
|
||||
this.#logger.log("checkpoint archive created", { size, options });
|
||||
|
||||
// Create image from checkpoint
|
||||
const container = this.#logger.debug(await $$`buildah from scratch`);
|
||||
const workingContainer = await buildah.from("scratch");
|
||||
const postFrom = performance.now();
|
||||
|
||||
this.#logger.debug(await $$`buildah add ${container} ${exportLocation} /`);
|
||||
await buildah.add(workingContainer.stdout, exportLocation, "/");
|
||||
const postAdd = performance.now();
|
||||
|
||||
this.#logger.debug(
|
||||
await $$`buildah config --annotation=io.kubernetes.cri-o.annotations.checkpoint.name=counter ${container}`
|
||||
);
|
||||
await buildah.config(workingContainer.stdout, [
|
||||
`io.kubernetes.cri-o.annotations.checkpoint.name=${shortCode}`,
|
||||
]);
|
||||
const postConfig = performance.now();
|
||||
|
||||
this.#logger.debug(await $$`buildah commit ${container} ${imageRef}`);
|
||||
await buildah.commit(workingContainer.stdout, imageRef);
|
||||
const postCommit = performance.now();
|
||||
|
||||
this.#logger.debug(await $$`buildah rm ${container}`);
|
||||
const postRm = performance.now();
|
||||
|
||||
if (this.simulatePushFailure) {
|
||||
if (performance.now() < this.simulatePushFailureSeconds * 1000) {
|
||||
this.#logger.error("Simulating push failure", { options });
|
||||
@@ -529,9 +527,7 @@ export class Checkpointer {
|
||||
}
|
||||
|
||||
// Push checkpoint image
|
||||
this.#logger.debug(
|
||||
await $$`buildah push --tls-verify=${String(this.registryTlsVerify)} ${imageRef}`
|
||||
);
|
||||
await buildah.push(imageRef, this.registryTlsVerify);
|
||||
const postPush = performance.now();
|
||||
|
||||
const perf = {
|
||||
@@ -540,8 +536,7 @@ export class Checkpointer {
|
||||
"buildah add": postAdd - postFrom,
|
||||
"buildah config": postConfig - postAdd,
|
||||
"buildah commit": postCommit - postConfig,
|
||||
"buildah rm": postRm - postCommit,
|
||||
"buildah push": postPush - postRm,
|
||||
"buildah push": postPush - postCommit,
|
||||
};
|
||||
|
||||
this.#logger.log("Checkpointed and pushed image to:", { location: imageRef, perf });
|
||||
@@ -554,24 +549,77 @@ export class Checkpointer {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isExecaChildProcess(error)) {
|
||||
if (error.isCanceled) {
|
||||
this.#logger.error("Checkpoint canceled", { options, error });
|
||||
if (error instanceof Exec.Result) {
|
||||
if (error.aborted) {
|
||||
this.#logger.error("Checkpoint canceled: Exec", { options });
|
||||
|
||||
return { success: false, reason: "CANCELED" };
|
||||
} else {
|
||||
this.#logger.error("Checkpoint command error", { options, error });
|
||||
|
||||
return { success: false, reason: "ERROR" };
|
||||
}
|
||||
|
||||
this.#logger.error("Checkpoint command error", { options, error });
|
||||
|
||||
return { success: false, reason: "ERROR" };
|
||||
}
|
||||
|
||||
this.#logger.error("Unhandled checkpoint error", { options, error });
|
||||
|
||||
return { success: false, reason: "ERROR" };
|
||||
} finally {
|
||||
this.#abortControllers.delete(runId);
|
||||
await cleanup();
|
||||
|
||||
if (controller.signal.aborted) {
|
||||
this.#logger.error("Checkpoint canceled: Cleanup", { options });
|
||||
|
||||
// Overrides any prior return value
|
||||
return { success: false, reason: "CANCELED" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #createDockerCheckpoint(
|
||||
abortSignal: AbortSignal,
|
||||
runId: string,
|
||||
exportLocation: string,
|
||||
leaveRunning: boolean,
|
||||
attemptNumber?: number
|
||||
) {
|
||||
const containterNameWithAttempt = this.#getRunContainerName(runId, attemptNumber);
|
||||
const exec = new Exec({ logger: this.#logger, abortSignal });
|
||||
|
||||
try {
|
||||
if (this.opts.forceSimulate || !this.#canCheckpoint) {
|
||||
this.#logger.log("Simulating checkpoint");
|
||||
|
||||
await exec.x("docker", ["pause", containterNameWithAttempt]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.simulateCheckpointFailure) {
|
||||
if (performance.now() < this.simulateCheckpointFailureSeconds * 1000) {
|
||||
this.#logger.error("Simulating checkpoint failure", {
|
||||
runId,
|
||||
exportLocation,
|
||||
leaveRunning,
|
||||
attemptNumber,
|
||||
});
|
||||
|
||||
throw new Error("SIMULATE_CHECKPOINT_FAILURE");
|
||||
}
|
||||
}
|
||||
|
||||
const args = ["checkpoint", "create"];
|
||||
|
||||
if (leaveRunning) {
|
||||
args.push("--leave-running");
|
||||
}
|
||||
|
||||
args.push(containterNameWithAttempt, exportLocation);
|
||||
|
||||
await exec.x("docker", args);
|
||||
} catch (error) {
|
||||
this.#logger.error("Failed while creating docker checkpoint", { exportLocation });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,4 +638,34 @@ export class Checkpointer {
|
||||
#getRunContainerName(suffix: string, attemptNumber?: number) {
|
||||
return `task-run-${suffix}${attemptNumber && attemptNumber > 1 ? `-att${attemptNumber}` : ""}`;
|
||||
}
|
||||
|
||||
#createTmpCleaner() {
|
||||
if (!boolFromEnv("TMP_CLEANER_ENABLED", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPaths = [Buildah.tmpDir, Crictl.checkpointDir].filter(Boolean);
|
||||
const pathsOverride = process.env.TMP_CLEANER_PATHS_OVERRIDE?.split(",").filter(Boolean) ?? [];
|
||||
const paths = pathsOverride.length ? pathsOverride : defaultPaths;
|
||||
|
||||
if (paths.length === 0) {
|
||||
this.#logger.error("TempFileCleaner enabled but no paths to clean", {
|
||||
defaultPaths,
|
||||
pathsOverride,
|
||||
TMP_CLEANER_PATHS_OVERRIDE: process.env.TMP_CLEANER_PATHS_OVERRIDE,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
const cleaner = new TempFileCleaner({
|
||||
paths,
|
||||
maxAgeMinutes: numFromEnv("TMP_CLEANER_MAX_AGE_MINUTES", 60),
|
||||
intervalSeconds: numFromEnv("TMP_CLEANER_INTERVAL_SECONDS", 300),
|
||||
leadingEdge: boolFromEnv("TMP_CLEANER_LEADING_EDGE", false),
|
||||
});
|
||||
|
||||
cleaner.start();
|
||||
|
||||
return cleaner;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
import { Exec } from "./exec";
|
||||
import { setTimeout } from "timers/promises";
|
||||
|
||||
interface TempFileCleanerOptions {
|
||||
paths: string[];
|
||||
maxAgeMinutes: number;
|
||||
intervalSeconds: number;
|
||||
leadingEdge?: boolean;
|
||||
}
|
||||
|
||||
export class TempFileCleaner {
|
||||
private enabled = false;
|
||||
|
||||
private logger: SimpleStructuredLogger;
|
||||
private exec: Exec;
|
||||
|
||||
constructor(private opts: TempFileCleanerOptions) {
|
||||
this.logger = new SimpleStructuredLogger("tmp-cleaner", undefined, { ...this.opts });
|
||||
this.exec = new Exec({ logger: this.logger });
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.logger.log("TempFileCleaner.start");
|
||||
this.enabled = true;
|
||||
|
||||
if (!this.opts.leadingEdge) {
|
||||
await this.wait();
|
||||
}
|
||||
|
||||
while (this.enabled) {
|
||||
try {
|
||||
await this.clean();
|
||||
} catch (error) {
|
||||
this.logger.error("error during tick", { error });
|
||||
}
|
||||
|
||||
await this.wait();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.logger.log("TempFileCleaner.stop");
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
private wait() {
|
||||
return setTimeout(this.opts.intervalSeconds * 1000);
|
||||
}
|
||||
|
||||
private async clean() {
|
||||
for (const path of this.opts.paths) {
|
||||
try {
|
||||
await this.cleanSingle(path);
|
||||
} catch (error) {
|
||||
this.logger.error("error while cleaning", { path, error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanSingle(startingPoint: string) {
|
||||
const maxAgeMinutes = this.opts.maxAgeMinutes;
|
||||
|
||||
const ignoreStartingPoint = ["!", "-path", startingPoint];
|
||||
const onlyDirectDescendants = ["-maxdepth", "1"];
|
||||
const onlyOldFiles = ["-mmin", `+${maxAgeMinutes}`];
|
||||
|
||||
const baseArgs = [
|
||||
startingPoint,
|
||||
...ignoreStartingPoint,
|
||||
...onlyDirectDescendants,
|
||||
...onlyOldFiles,
|
||||
];
|
||||
|
||||
const duArgs = ["-exec", "du", "-ch", "{}", "+"];
|
||||
const rmArgs = ["-exec", "rm", "-rf", "{}", "+"];
|
||||
|
||||
const du = this.x("find", [...baseArgs, ...duArgs]);
|
||||
const duOutput = await du;
|
||||
|
||||
const duLines = duOutput.stdout.trim().split("\n");
|
||||
const fileCount = duLines.length - 1; // last line is the total
|
||||
const fileSize = duLines.at(-1)?.trim().split(/\s+/)[0];
|
||||
|
||||
if (fileCount === 0) {
|
||||
this.logger.log("nothing to delete", { startingPoint, maxAgeMinutes });
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("deleting old files", { fileCount, fileSize, startingPoint, maxAgeMinutes });
|
||||
|
||||
const rm = this.x("find", [...baseArgs, ...rmArgs]);
|
||||
const rmOutput = await rm;
|
||||
|
||||
if (rmOutput.stderr.length > 0) {
|
||||
this.logger.error("delete unsuccessful", { rmOutput });
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("deleted old files", { fileCount, fileSize, startingPoint, maxAgeMinutes });
|
||||
}
|
||||
|
||||
private get x() {
|
||||
return this.exec.x.bind(this.exec);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
import { randomUUID } from "crypto";
|
||||
import { homedir } from "os";
|
||||
import { type Result, x } from "tinyexec";
|
||||
|
||||
class TinyResult {
|
||||
pid?: number;
|
||||
exitCode?: number;
|
||||
aborted: boolean;
|
||||
killed: boolean;
|
||||
|
||||
constructor(result: Result) {
|
||||
this.pid = result.pid;
|
||||
this.exitCode = result.exitCode;
|
||||
this.aborted = result.aborted;
|
||||
this.killed = result.killed;
|
||||
}
|
||||
}
|
||||
|
||||
interface ExecOptions {
|
||||
logger?: SimpleStructuredLogger;
|
||||
abortSignal?: AbortSignal;
|
||||
logOutput?: boolean;
|
||||
trimArgs?: boolean;
|
||||
neverThrow?: boolean;
|
||||
}
|
||||
|
||||
export class Exec {
|
||||
private logger: SimpleStructuredLogger;
|
||||
private abortSignal: AbortSignal | undefined;
|
||||
|
||||
private logOutput: boolean;
|
||||
private trimArgs: boolean;
|
||||
private neverThrow: boolean;
|
||||
|
||||
constructor(opts: ExecOptions) {
|
||||
this.logger = opts.logger ?? new SimpleStructuredLogger("exec");
|
||||
this.abortSignal = opts.abortSignal;
|
||||
|
||||
this.logOutput = opts.logOutput ?? true;
|
||||
this.trimArgs = opts.trimArgs ?? true;
|
||||
this.neverThrow = opts.neverThrow ?? false;
|
||||
}
|
||||
|
||||
async x(
|
||||
command: string,
|
||||
args?: string[],
|
||||
opts?: { neverThrow?: boolean; ignoreAbort?: boolean }
|
||||
) {
|
||||
const argsTrimmed = this.trimArgs ? args?.map((arg) => arg.trim()) : args;
|
||||
|
||||
const commandWithFirstArg = `${command}${argsTrimmed?.length ? ` ${argsTrimmed[0]}` : ""}`;
|
||||
this.logger.debug(`exec: ${commandWithFirstArg}`, { command, args, argsTrimmed });
|
||||
|
||||
const result = x(command, argsTrimmed, {
|
||||
signal: opts?.ignoreAbort ? undefined : this.abortSignal,
|
||||
// We don't use this as it doesn't cover killed and aborted processes
|
||||
// throwOnError: true,
|
||||
});
|
||||
|
||||
const output = await result;
|
||||
|
||||
const metadata = {
|
||||
command,
|
||||
argsRaw: args,
|
||||
argsTrimmed,
|
||||
globalOpts: {
|
||||
trimArgs: this.trimArgs,
|
||||
neverThrow: this.neverThrow,
|
||||
hasAbortSignal: !!this.abortSignal,
|
||||
},
|
||||
localOpts: opts,
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
pid: result.pid,
|
||||
exitCode: result.exitCode,
|
||||
aborted: result.aborted,
|
||||
killed: result.killed,
|
||||
};
|
||||
|
||||
if (this.logOutput) {
|
||||
this.logger.debug(`output: ${commandWithFirstArg}`, metadata);
|
||||
}
|
||||
|
||||
if (this.neverThrow || opts?.neverThrow) {
|
||||
return output;
|
||||
}
|
||||
|
||||
if (result.aborted) {
|
||||
this.logger.error(`aborted: ${commandWithFirstArg}`, metadata);
|
||||
throw new TinyResult(result);
|
||||
}
|
||||
|
||||
if (result.killed) {
|
||||
this.logger.error(`killed: ${commandWithFirstArg}`, metadata);
|
||||
throw new TinyResult(result);
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
this.logger.error(`non-zero exit: ${commandWithFirstArg}`, metadata);
|
||||
throw new TinyResult(result);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static Result = TinyResult;
|
||||
}
|
||||
|
||||
interface BuildahOptions {
|
||||
id?: string;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
export class Buildah {
|
||||
private id: string;
|
||||
private logger: SimpleStructuredLogger;
|
||||
private exec: Exec;
|
||||
|
||||
private containers = new Set<string>();
|
||||
private images = new Set<string>();
|
||||
|
||||
constructor(opts: BuildahOptions) {
|
||||
this.id = opts.id ?? randomUUID();
|
||||
this.logger = new SimpleStructuredLogger("buildah", undefined, { id: this.id });
|
||||
|
||||
this.exec = new Exec({
|
||||
logger: this.logger,
|
||||
abortSignal: opts.abortSignal,
|
||||
});
|
||||
|
||||
this.logger.log("initiaized", { opts });
|
||||
}
|
||||
|
||||
private get x() {
|
||||
return this.exec.x.bind(this.exec);
|
||||
}
|
||||
|
||||
async from(baseImage: string) {
|
||||
const output = await this.x("buildah", ["from", baseImage]);
|
||||
this.containers.add(output.stdout);
|
||||
return output;
|
||||
}
|
||||
|
||||
async add(container: string, src: string, dest: string) {
|
||||
return await this.x("buildah", ["add", container, src, dest]);
|
||||
}
|
||||
|
||||
async config(container: string, annotations: string[]) {
|
||||
const args = ["config"];
|
||||
|
||||
for (const annotation of annotations) {
|
||||
args.push(`--annotation=${annotation}`);
|
||||
}
|
||||
|
||||
args.push(container);
|
||||
|
||||
return await this.x("buildah", args);
|
||||
}
|
||||
|
||||
async commit(container: string, imageRef: string) {
|
||||
const output = await this.x("buildah", ["commit", container, imageRef]);
|
||||
this.images.add(output.stdout);
|
||||
return output;
|
||||
}
|
||||
|
||||
async push(imageRef: string, registryTlsVerify?: boolean) {
|
||||
return await this.x("buildah", [
|
||||
"push",
|
||||
`--tls-verify=${String(!!registryTlsVerify)}`,
|
||||
imageRef,
|
||||
]);
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.containers.size > 0) {
|
||||
try {
|
||||
const output = await this.x("buildah", ["rm", ...this.containers], { ignoreAbort: true });
|
||||
this.containers.clear();
|
||||
|
||||
if (output.stderr.length > 0) {
|
||||
this.logger.error("failed to remove some containers", { output });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("failed to clean up containers", { error, containers: this.containers });
|
||||
}
|
||||
} else {
|
||||
this.logger.debug("no containers to clean up");
|
||||
}
|
||||
|
||||
if (this.images.size > 0) {
|
||||
try {
|
||||
const output = await this.x("buildah", ["rmi", ...this.images], { ignoreAbort: true });
|
||||
this.images.clear();
|
||||
|
||||
if (output.stderr.length > 0) {
|
||||
this.logger.error("failed to remove some images", { output });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("failed to clean up images", { error, images: this.images });
|
||||
}
|
||||
} else {
|
||||
this.logger.debug("no images to clean up");
|
||||
}
|
||||
}
|
||||
|
||||
static async canLogin(registryHost: string) {
|
||||
try {
|
||||
await x("buildah", ["login", "--get-login", registryHost], { throwOnError: true });
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static get tmpDir() {
|
||||
return process.env.TMPDIR ?? "/var/tmp";
|
||||
}
|
||||
|
||||
static get storageRootDir() {
|
||||
return process.getuid?.() === 0
|
||||
? "/var/lib/containers/storage"
|
||||
: `${homedir()}/.local/share/containers/storage`;
|
||||
}
|
||||
}
|
||||
|
||||
interface CrictlOptions {
|
||||
id?: string;
|
||||
abortSignal?: AbortSignal;
|
||||
}
|
||||
|
||||
export class Crictl {
|
||||
private id: string;
|
||||
private logger: SimpleStructuredLogger;
|
||||
private exec: Exec;
|
||||
|
||||
private archives = new Set<string>();
|
||||
|
||||
constructor(opts: CrictlOptions) {
|
||||
this.id = opts.id ?? randomUUID();
|
||||
this.logger = new SimpleStructuredLogger("crictl", undefined, { id: this.id });
|
||||
|
||||
this.exec = new Exec({
|
||||
logger: this.logger,
|
||||
abortSignal: opts.abortSignal,
|
||||
});
|
||||
|
||||
this.logger.log("initiaized", { opts });
|
||||
}
|
||||
|
||||
private get x() {
|
||||
return this.exec.x.bind(this.exec);
|
||||
}
|
||||
|
||||
async ps(containerName: string, quiet?: boolean) {
|
||||
return await this.x("crictl", ["ps", "--name", containerName, quiet ? "--quiet" : ""]);
|
||||
}
|
||||
|
||||
async checkpoint(containerId: string, exportLocation: string) {
|
||||
const output = await this.x("crictl", [
|
||||
"checkpoint",
|
||||
`--export=${exportLocation}`,
|
||||
containerId,
|
||||
]);
|
||||
this.archives.add(exportLocation);
|
||||
return output;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.archives.size > 0) {
|
||||
try {
|
||||
const output = await this.x("rm", ["-v", ...this.archives], { ignoreAbort: true });
|
||||
this.archives.clear();
|
||||
|
||||
if (output.stderr.length > 0) {
|
||||
this.logger.error("failed to remove some archives", { output });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error("failed to clean up archives", { error, archives: this.archives });
|
||||
}
|
||||
} else {
|
||||
this.logger.debug("no archives to clean up");
|
||||
}
|
||||
}
|
||||
|
||||
static getExportLocation(identifier: string) {
|
||||
return `${this.checkpointDir}/${identifier}.tar`;
|
||||
}
|
||||
|
||||
static get checkpointDir() {
|
||||
return process.env.CRI_CHECKPOINT_DIR ?? "/checkpoints";
|
||||
}
|
||||
}
|
||||
+744
-397
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
export const boolFromEnv = (env: string, defaultValue: boolean): boolean => {
|
||||
const value = process.env[env];
|
||||
|
||||
if (!value) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return ["1", "true"].includes(value);
|
||||
};
|
||||
|
||||
export const numFromEnv = (env: string, defaultValue: number): number => {
|
||||
const value = process.env[env];
|
||||
|
||||
if (!value) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return parseInt(value, 10);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ const OTEL_EXPORTER_OTLP_ENDPOINT =
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://0.0.0.0:4318";
|
||||
|
||||
const FORCE_CHECKPOINT_SIMULATION = ["1", "true"].includes(
|
||||
process.env.FORCE_CHECKPOINT_SIMULATION ?? "true"
|
||||
process.env.FORCE_CHECKPOINT_SIMULATION ?? "false"
|
||||
);
|
||||
|
||||
const logger = new SimpleLogger(`[${MACHINE_NAME}]`);
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import * as k8s from "@kubernetes/client-node";
|
||||
import {
|
||||
EnvironmentType,
|
||||
MachinePreset,
|
||||
PostStartCauses,
|
||||
PreStopCauses,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import {
|
||||
ProviderShell,
|
||||
SimpleLogger,
|
||||
TaskOperations,
|
||||
TaskOperationsCreateOptions,
|
||||
TaskOperationsIndexOptions,
|
||||
TaskOperationsPrePullDeploymentOptions,
|
||||
TaskOperationsRestoreOptions,
|
||||
} from "@trigger.dev/core/v3/apps";
|
||||
import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import {
|
||||
MachinePreset,
|
||||
PostStartCauses,
|
||||
PreStopCauses,
|
||||
EnvironmentType,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { TaskMonitor } from "./taskMonitor";
|
||||
import { PodCleaner } from "./podCleaner";
|
||||
import { TaskMonitor } from "./taskMonitor";
|
||||
import { UptimeHeartbeat } from "./uptimeHeartbeat";
|
||||
|
||||
const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local";
|
||||
const NODE_NAME = process.env.NODE_NAME || "local";
|
||||
const OTEL_EXPORTER_OTLP_ENDPOINT =
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318";
|
||||
const COORDINATOR_HOST = process.env.COORDINATOR_HOST ?? undefined;
|
||||
const COORDINATOR_PORT = process.env.COORDINATOR_PORT ?? undefined;
|
||||
const KUBERNETES_NAMESPACE = process.env.KUBERNETES_NAMESPACE ?? "default";
|
||||
|
||||
const POD_CLEANER_INTERVAL_SECONDS = Number(process.env.POD_CLEANER_INTERVAL_SECONDS || "300");
|
||||
|
||||
@@ -31,6 +34,9 @@ const UPTIME_MAX_PENDING_RUNS = Number(process.env.UPTIME_MAX_PENDING_RUNS || "2
|
||||
const UPTIME_MAX_PENDING_INDECES = Number(process.env.UPTIME_MAX_PENDING_INDECES || "10");
|
||||
const UPTIME_MAX_PENDING_ERRORS = Number(process.env.UPTIME_MAX_PENDING_ERRORS || "10");
|
||||
|
||||
const POD_EPHEMERAL_STORAGE_SIZE_LIMIT = process.env.POD_EPHEMERAL_STORAGE_SIZE_LIMIT || "10Gi";
|
||||
const POD_EPHEMERAL_STORAGE_SIZE_REQUEST = process.env.POD_EPHEMERAL_STORAGE_SIZE_REQUEST || "2Gi";
|
||||
|
||||
const logger = new SimpleLogger(`[${NODE_NAME}]`);
|
||||
logger.log(`running in ${RUNTIME_ENV} mode`);
|
||||
|
||||
@@ -45,19 +51,22 @@ type ResourceQuantities = {
|
||||
};
|
||||
|
||||
class KubernetesTaskOperations implements TaskOperations {
|
||||
#namespace: Namespace;
|
||||
#namespace: Namespace = {
|
||||
metadata: {
|
||||
name: "default",
|
||||
},
|
||||
};
|
||||
|
||||
#k8sApi: {
|
||||
core: k8s.CoreV1Api;
|
||||
batch: k8s.BatchV1Api;
|
||||
apps: k8s.AppsV1Api;
|
||||
};
|
||||
|
||||
constructor(namespace = "default") {
|
||||
this.#namespace = {
|
||||
metadata: {
|
||||
name: namespace,
|
||||
},
|
||||
};
|
||||
constructor(opts: { namespace?: string } = {}) {
|
||||
if (opts.namespace) {
|
||||
this.#namespace.metadata.name = opts.namespace;
|
||||
}
|
||||
|
||||
this.#k8sApi = this.#createK8sApi();
|
||||
}
|
||||
@@ -229,16 +238,7 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
command: ["/bin/sh", "-c"],
|
||||
args: ["printenv COORDINATOR_HOST | tee /etc/taskinfo/coordinator-host"],
|
||||
env: [
|
||||
{
|
||||
name: "COORDINATOR_HOST",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "status.hostIP",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
env: this.#coordinatorEnvVars,
|
||||
volumeMounts: [
|
||||
{
|
||||
name: "taskinfo",
|
||||
@@ -399,16 +399,51 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
|
||||
get #defaultResourceRequests(): ResourceQuantities {
|
||||
return {
|
||||
"ephemeral-storage": "2Gi",
|
||||
"ephemeral-storage": POD_EPHEMERAL_STORAGE_SIZE_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
get #defaultResourceLimits(): ResourceQuantities {
|
||||
return {
|
||||
"ephemeral-storage": "10Gi",
|
||||
"ephemeral-storage": POD_EPHEMERAL_STORAGE_SIZE_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
get #coordinatorHostEnvVar(): k8s.V1EnvVar {
|
||||
return COORDINATOR_HOST
|
||||
? {
|
||||
name: "COORDINATOR_HOST",
|
||||
value: COORDINATOR_HOST,
|
||||
}
|
||||
: {
|
||||
name: "COORDINATOR_HOST",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "status.hostIP",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get #coordinatorPortEnvVar(): k8s.V1EnvVar | undefined {
|
||||
if (COORDINATOR_PORT) {
|
||||
return {
|
||||
name: "COORDINATOR_PORT",
|
||||
value: COORDINATOR_PORT,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get #coordinatorEnvVars(): k8s.V1EnvVar[] {
|
||||
const envVars = [this.#coordinatorHostEnvVar];
|
||||
|
||||
if (this.#coordinatorPortEnvVar) {
|
||||
envVars.push(this.#coordinatorPortEnvVar);
|
||||
}
|
||||
|
||||
return envVars;
|
||||
}
|
||||
|
||||
#getSharedEnv(envId: string): k8s.V1EnvVar[] {
|
||||
return [
|
||||
{
|
||||
@@ -435,14 +470,6 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "COORDINATOR_HOST",
|
||||
valueFrom: {
|
||||
fieldRef: {
|
||||
fieldPath: "status.hostIP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MACHINE_NAME",
|
||||
valueFrom: {
|
||||
@@ -451,6 +478,7 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
},
|
||||
},
|
||||
},
|
||||
...this.#coordinatorEnvVars,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -623,7 +651,9 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
}
|
||||
|
||||
const provider = new ProviderShell({
|
||||
tasks: new KubernetesTaskOperations(),
|
||||
tasks: new KubernetesTaskOperations({
|
||||
namespace: KUBERNETES_NAMESPACE,
|
||||
}),
|
||||
type: "kubernetes",
|
||||
});
|
||||
|
||||
@@ -631,6 +661,7 @@ provider.listen();
|
||||
|
||||
const taskMonitor = new TaskMonitor({
|
||||
runtimeEnv: RUNTIME_ENV,
|
||||
namespace: KUBERNETES_NAMESPACE,
|
||||
onIndexFailure: async (deploymentId, details) => {
|
||||
logger.log("Indexing failed", { deploymentId, details });
|
||||
|
||||
@@ -663,7 +694,7 @@ taskMonitor.start();
|
||||
|
||||
const podCleaner = new PodCleaner({
|
||||
runtimeEnv: RUNTIME_ENV,
|
||||
namespace: "default",
|
||||
namespace: KUBERNETES_NAMESPACE,
|
||||
intervalInSeconds: POD_CLEANER_INTERVAL_SECONDS,
|
||||
});
|
||||
|
||||
@@ -672,7 +703,7 @@ podCleaner.start();
|
||||
if (UPTIME_HEARTBEAT_URL) {
|
||||
const uptimeHeartbeat = new UptimeHeartbeat({
|
||||
runtimeEnv: RUNTIME_ENV,
|
||||
namespace: "default",
|
||||
namespace: KUBERNETES_NAMESPACE,
|
||||
intervalInSeconds: UPTIME_INTERVAL_SECONDS,
|
||||
pingUrl: UPTIME_HEARTBEAT_URL,
|
||||
maxPendingRuns: UPTIME_MAX_PENDING_RUNS,
|
||||
|
||||
@@ -3,13 +3,14 @@ import { SimpleLogger } from "@trigger.dev/core/v3/apps";
|
||||
import { EXIT_CODE_ALREADY_HANDLED, EXIT_CODE_CHILD_NONZERO } from "@trigger.dev/core/v3/apps";
|
||||
import { setTimeout } from "timers/promises";
|
||||
import PQueue from "p-queue";
|
||||
import type { Prettify } from "@trigger.dev/core/v3";
|
||||
import { TaskRunErrorCodes, type Prettify, type TaskRunInternalError } from "@trigger.dev/core/v3";
|
||||
|
||||
type FailureDetails = Prettify<{
|
||||
exitCode: number;
|
||||
reason: string;
|
||||
logs: string;
|
||||
overrideCompletion: boolean;
|
||||
errorCode: TaskRunInternalError["code"];
|
||||
}>;
|
||||
|
||||
type IndexFailureHandler = (deploymentId: string, details: FailureDetails) => Promise<any>;
|
||||
@@ -159,19 +160,27 @@ export class TaskMonitor {
|
||||
|
||||
let reason = rawReason || "Unknown error";
|
||||
let logs = rawLogs || "";
|
||||
let overrideCompletion = false;
|
||||
|
||||
/** This will only override existing task errors. It will not crash the run. */
|
||||
let onlyOverrideExistingError = exitCode === EXIT_CODE_CHILD_NONZERO;
|
||||
|
||||
let errorCode: TaskRunInternalError["code"] = TaskRunErrorCodes.POD_UNKNOWN_ERROR;
|
||||
|
||||
switch (rawReason) {
|
||||
case "Error":
|
||||
reason = "Unknown error.";
|
||||
errorCode = TaskRunErrorCodes.POD_UNKNOWN_ERROR;
|
||||
break;
|
||||
case "Evicted":
|
||||
if (message.startsWith("Pod ephemeral local storage usage")) {
|
||||
reason = "Storage limit exceeded.";
|
||||
errorCode = TaskRunErrorCodes.DISK_SPACE_EXCEEDED;
|
||||
} else if (message) {
|
||||
reason = `Evicted: ${message}`;
|
||||
errorCode = TaskRunErrorCodes.POD_EVICTED;
|
||||
} else {
|
||||
reason = "Evicted for unknown reason.";
|
||||
errorCode = TaskRunErrorCodes.POD_EVICTED;
|
||||
}
|
||||
|
||||
if (logs.startsWith("failed to try resolving symlinks")) {
|
||||
@@ -179,10 +188,9 @@ export class TaskMonitor {
|
||||
}
|
||||
break;
|
||||
case "OOMKilled":
|
||||
overrideCompletion = true;
|
||||
reason = `${
|
||||
exitCode === EXIT_CODE_CHILD_NONZERO ? "Child process" : "Parent process"
|
||||
} ran out of memory! Try choosing a machine preset with more memory for this task.`;
|
||||
reason =
|
||||
"[TaskMonitor] Your task ran out of memory. Try increasing the machine specs. If this doesn't fix it there might be a memory leak.";
|
||||
errorCode = TaskRunErrorCodes.TASK_PROCESS_OOM_KILLED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -192,7 +200,8 @@ export class TaskMonitor {
|
||||
exitCode,
|
||||
reason,
|
||||
logs,
|
||||
overrideCompletion,
|
||||
overrideCompletion: onlyOverrideExistingError,
|
||||
errorCode,
|
||||
} satisfies FailureDetails;
|
||||
|
||||
const app = pod.metadata?.labels?.app;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev"
|
||||
"dev": "wrangler dev",
|
||||
"dry-run:staging": "wrangler deploy --dry-run --outdir=dist --env staging"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240512.0",
|
||||
@@ -15,7 +16,7 @@
|
||||
"@aws-sdk/client-sqs": "^3.445.0",
|
||||
"@trigger.dev/core": "workspace:*",
|
||||
"ulidx": "^2.2.1",
|
||||
"zod": "3.22.3",
|
||||
"zod": "3.23.8",
|
||||
"zod-error": "1.5.0"
|
||||
}
|
||||
}
|
||||
+2
-20
@@ -17,12 +17,9 @@ export interface Env {
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
if (!env.REWRITE_HOSTNAME) throw new Error("Missing REWRITE_HOSTNAME");
|
||||
console.log("url", request.url);
|
||||
|
||||
if (!queueingIsEnabled(env)) {
|
||||
console.log("Missing AWS credentials. Passing through to the origin.");
|
||||
return redirectToOrigin(request, env);
|
||||
return fetch(request);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
@@ -42,25 +39,10 @@ export default {
|
||||
}
|
||||
|
||||
//the same request but with the hostname (and port) changed
|
||||
return redirectToOrigin(request, env);
|
||||
return fetch(request);
|
||||
},
|
||||
};
|
||||
|
||||
function redirectToOrigin(request: Request, env: Env) {
|
||||
const newUrl = new URL(request.url);
|
||||
newUrl.hostname = env.REWRITE_HOSTNAME;
|
||||
newUrl.port = env.REWRITE_PORT || newUrl.port;
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
};
|
||||
|
||||
console.log("rewritten url", newUrl.toString());
|
||||
return fetch(newUrl.toString(), requestInit);
|
||||
}
|
||||
|
||||
function queueingIsEnabled(env: Env) {
|
||||
return (
|
||||
env.AWS_SQS_ACCESS_KEY_ID &&
|
||||
|
||||
@@ -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,15 @@
|
||||
export function SideMenuRightClosedIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="12" y="4" width="1" height="12" fill="currentColor" />
|
||||
<rect x="2.5" y="3.5" width="15" height="13" rx="2.5" stroke="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export function TimedOutIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 2H8V3H9V4.07089C5.60771 4.55612 3 7.47353 3 11C3 14.866 6.13401 18 10 18C13.866 18 17 14.866 17 11C17 7.47353 14.3923 4.55612 11 4.07089V3H12V2H11H9ZM13.7603 3.36575C14.7218 2.40422 16.2807 2.40422 17.2422 3.36575C18.2038 4.32727 18.2038 5.8862 17.2422 6.84772L17.1462 6.94375C17.1251 6.96488 17.0908 6.96488 17.0697 6.94375L13.6642 3.53831C13.6431 3.51717 13.6431 3.4829 13.6642 3.46177L13.7603 3.36575ZM6.28876 3.58524C6.33584 3.53816 6.33584 3.46184 6.28876 3.41476L6.23971 3.36571C5.27819 2.40419 3.71925 2.40419 2.75773 3.36571C1.79621 4.32723 1.79621 5.88616 2.75773 6.84769L2.80678 6.89674C2.85386 6.94382 2.93019 6.94381 2.97726 6.89674L6.28876 3.58524ZM14.5858 17L16 15.5858L16.7071 16.2929C17.0976 16.6834 17.0976 17.3166 16.7071 17.7071C16.3166 18.0976 15.6834 18.0976 15.2929 17.7071L14.5858 17ZM5.42297 17L4.00875 15.5858L3.30165 16.2929C2.91112 16.6834 2.91112 17.3166 3.30165 17.7071C3.69217 18.0977 4.32534 18.0977 4.71586 17.7071L5.42297 17ZM6.29289 7.29289C6.68342 6.90237 7.31658 6.90237 7.70711 7.29289L10 9.58579L12.2929 7.29289C12.6834 6.90237 13.3166 6.90237 13.7071 7.29289C14.0976 7.68342 14.0976 8.31658 13.7071 8.70711L11.4142 11L13.7071 13.2929C14.0976 13.6834 14.0976 14.3166 13.7071 14.7071C13.3166 15.0976 12.6834 15.0976 12.2929 14.7071L10 12.4142L7.70711 14.7071C7.31658 15.0976 6.68342 15.0976 6.29289 14.7071C5.90237 14.3166 5.90237 13.6834 6.29289 13.2929L8.58579 11L6.29289 8.70711C5.90237 8.31658 5.90237 7.68342 6.29289 7.29289Z"
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { HomeIcon } from "@heroicons/react/20/solid";
|
||||
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
|
||||
import { LinkButton } from "./primitives/Buttons";
|
||||
import { Header1, Header3 } from "./primitives/Headers";
|
||||
import { motion } from "framer-motion";
|
||||
import { friendlyErrorDisplay } from "~/utils/httpErrors";
|
||||
import { LinkButton } from "./primitives/Buttons";
|
||||
import { Header1 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import Spline from "@splinetool/react-spline";
|
||||
|
||||
type ErrorDisplayOptions = {
|
||||
button?: {
|
||||
@@ -39,12 +43,27 @@ type DisplayOptionsProps = {
|
||||
|
||||
export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Header1 className="mb-4 border-b border-charcoal-800 pb-4">{title}</Header1>
|
||||
{message && <Header3>{message}</Header3>}
|
||||
<LinkButton to={button ? button.to : "/"} variant="primary/medium" className="mt-8">
|
||||
{button ? button.title : "Home"}
|
||||
</LinkButton>
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center bg-[#16181C]">
|
||||
<div className="z-10 mt-[30vh] flex flex-col items-center gap-8">
|
||||
<Header1>{title}</Header1>
|
||||
{message && <Paragraph>{message}</Paragraph>}
|
||||
<LinkButton
|
||||
to={button ? button.to : "/"}
|
||||
shortcut={{ modifiers: ["mod"], key: "g" }}
|
||||
variant="primary/medium"
|
||||
LeadingIcon={HomeIcon}
|
||||
>
|
||||
{button ? button.title : "Go to homepage"}
|
||||
</LinkButton>
|
||||
</div>
|
||||
<motion.div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5, duration: 2, ease: "easeOut" }}
|
||||
>
|
||||
<Spline scene="https://prod.spline.design/wRly8TZN-e0Twb8W/scene.splinecode" />
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
ChevronRightIcon,
|
||||
EnvelopeIcon,
|
||||
LifebuoyIcon,
|
||||
LightBulbIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { EnvelopeIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
import { ActivityIcon } from "lucide-react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { type FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { docsTroubleshootingPath } from "~/utils/pathBuilder";
|
||||
import { Button, LinkButton } from "./primitives/Buttons";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog";
|
||||
import { Fieldset } from "./primitives/Fieldset";
|
||||
import { FormButtons } from "./primitives/FormButtons";
|
||||
import { FormError } from "./primitives/FormError";
|
||||
import { Header1 } from "./primitives/Headers";
|
||||
import { Icon } from "./primitives/Icon";
|
||||
import { InfoPanel } from "./primitives/InfoPanel";
|
||||
import { InputGroup } from "./primitives/InputGroup";
|
||||
import { Label } from "./primitives/Label";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import { Select, SelectItem } from "./primitives/Select";
|
||||
import { Sheet, SheetBody, SheetContent, SheetTrigger } from "./primitives/Sheet";
|
||||
import { TextArea } from "./primitives/TextArea";
|
||||
import { TextLink } from "./primitives/TextLink";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
|
||||
type FeedbackProps = {
|
||||
button: ReactNode;
|
||||
@@ -37,161 +30,113 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
|
||||
const location = useLocation();
|
||||
const lastSubmission = useActionData();
|
||||
const navigation = useNavigation();
|
||||
const [type, setType] = useState<FeedbackType>(defaultValue);
|
||||
|
||||
const [form, { path, feedbackType, message }] = useForm({
|
||||
id: "accept-invite",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
shouldRevalidate: "onInput",
|
||||
});
|
||||
|
||||
if (
|
||||
open &&
|
||||
navigation.formAction === "/resources/feedback" &&
|
||||
form.error === undefined &&
|
||||
form.errors.length === 0
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (
|
||||
navigation.formAction === "/resources/feedback" &&
|
||||
navigation.state === "loading" &&
|
||||
form.error === undefined &&
|
||||
form.errors.length === 0
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [navigation, form]);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild={true}>{button}</SheetTrigger>
|
||||
<SheetContent className="@container">
|
||||
<SheetBody className="flex h-full flex-col justify-between">
|
||||
<LinkBanner
|
||||
title="Join our Discord community"
|
||||
icon={<DiscordIcon className="size-9" />}
|
||||
to="https://trigger.dev/discord"
|
||||
className="hover:border-text-link"
|
||||
>
|
||||
<Paragraph>The quickest way to get answers from the Trigger.dev community.</Paragraph>
|
||||
</LinkBanner>
|
||||
<LinkBanner
|
||||
title="Book a 15 min chat with the founders"
|
||||
icon={<CalendarDaysIcon className="size-9 text-green-500" />}
|
||||
to="https://cal.com/team/triggerdotdev/founders-call"
|
||||
className="hover:border-green-500"
|
||||
>
|
||||
<Paragraph>Have a question or want to chat? Book a time to talk with us.</Paragraph>
|
||||
</LinkBanner>
|
||||
<LinkBanner
|
||||
title="Suggest a feature"
|
||||
icon={<LightBulbIcon className="size-9 text-sun-500" />}
|
||||
to="https://feedback.trigger.dev/"
|
||||
className="hover:border-sun-400"
|
||||
>
|
||||
<Paragraph>Have an idea for a new feature or improvement? Let us know!</Paragraph>
|
||||
</LinkBanner>
|
||||
<LinkBanner
|
||||
title="Troubleshooting"
|
||||
icon={<LifebuoyIcon className="size-9 text-rose-500" />}
|
||||
>
|
||||
<Paragraph>
|
||||
If you're having trouble, check out our troubleshooting guide or the Trigger.dev
|
||||
Status page.
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{button}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>Contact us</DialogHeader>
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Icon icon={EnvelopeIcon} className="size-10 min-w-[2.5rem] text-blue-500" />
|
||||
<Paragraph variant="base/bright">
|
||||
How can we help? We read every message and will respond as quickly as we can.
|
||||
</Paragraph>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LinkButton
|
||||
to={docsTroubleshootingPath("")}
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
>
|
||||
Troubleshooting Docs
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
to={"https://status.trigger.dev/"}
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={ActivityIcon}
|
||||
>
|
||||
Trigger.dev Status
|
||||
</LinkButton>
|
||||
</div>
|
||||
</LinkBanner>
|
||||
<LinkBanner
|
||||
title="Send us an email"
|
||||
icon={<EnvelopeIcon className="size-9 text-blue-500" />}
|
||||
>
|
||||
<Paragraph>We read every message and respond quickly.</Paragraph>
|
||||
<Form method="post" action="/resources/feedback" {...form.props} className="w-full">
|
||||
<Fieldset className="max-w-full gap-y-3">
|
||||
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
|
||||
<InputGroup className="max-w-full">
|
||||
<Select
|
||||
{...conform.select(feedbackType)}
|
||||
variant="tertiary/medium"
|
||||
defaultValue={defaultValue}
|
||||
placeholder="Select type"
|
||||
text={(value) => feedbackTypeLabel[value]}
|
||||
dropdownIcon
|
||||
</div>
|
||||
<hr className="border-charcoal-800" />
|
||||
<Form method="post" action="/resources/feedback" {...form.props} className="w-full">
|
||||
<Fieldset className="max-w-full gap-y-3">
|
||||
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
|
||||
<InputGroup className="max-w-full">
|
||||
{type === "feature" && (
|
||||
<InfoPanel
|
||||
icon={InformationCircleIcon}
|
||||
iconClassName="text-blue-500"
|
||||
panelClassName="w-full mb-2"
|
||||
>
|
||||
{Object.entries(feedbackTypeLabel).map(([name, title]) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup className="max-w-full">
|
||||
<Label>Message</Label>
|
||||
<TextArea {...conform.textarea(message)} />
|
||||
<FormError id={message.errorId}>{message.error}</FormError>
|
||||
</InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
<div className="flex w-full justify-end">
|
||||
<FormButtons
|
||||
className="m-0 w-max"
|
||||
confirmButton={
|
||||
<Button type="submit" variant="tertiary/medium">
|
||||
Send message
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</LinkBanner>
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkBanner({
|
||||
className,
|
||||
icon,
|
||||
title,
|
||||
children,
|
||||
to,
|
||||
}: {
|
||||
className?: string;
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
children?: ReactNode;
|
||||
to?: string;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
"group/banner mb-4 flex w-full items-center justify-between rounded-md border border-grid-bright bg-charcoal-750 p-4 transition",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-start gap-4">
|
||||
<span>{icon}</span>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Header1 className="text-2xl font-semibold text-text-bright">{title}</Header1>
|
||||
{children}
|
||||
<Paragraph variant="small">
|
||||
All our feature requests are public and voted on by the community. The best
|
||||
way to submit your feature request is to{" "}
|
||||
<TextLink to="https://feedback.trigger.dev">
|
||||
post it to our feedback forum
|
||||
</TextLink>
|
||||
.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
)}
|
||||
{type === "help" && (
|
||||
<InfoPanel
|
||||
icon={InformationCircleIcon}
|
||||
iconClassName="text-blue-500"
|
||||
panelClassName="w-full mb-2"
|
||||
>
|
||||
<Paragraph variant="small">
|
||||
The quickest way to get answers from the Trigger.dev team and community is to{" "}
|
||||
<TextLink to="https://trigger.dev/discord">ask in our Discord</TextLink>.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
)}
|
||||
<Select
|
||||
{...conform.select(feedbackType)}
|
||||
variant="tertiary/medium"
|
||||
value={type}
|
||||
defaultValue={type}
|
||||
setValue={(v) => setType(v as FeedbackType)}
|
||||
placeholder="Select type"
|
||||
text={(value) => feedbackTypeLabel[value as FeedbackType]}
|
||||
dropdownIcon
|
||||
>
|
||||
{Object.entries(feedbackTypeLabel).map(([name, title]) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup className="max-w-full">
|
||||
<Label>Message</Label>
|
||||
<TextArea {...conform.textarea(message)} />
|
||||
<FormError id={message.errorId}>{message.error}</FormError>
|
||||
</InputGroup>
|
||||
<FormError>{form.error}</FormError>
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button type="submit" variant="primary/medium">
|
||||
Send message
|
||||
</Button>
|
||||
}
|
||||
cancelButton={
|
||||
<DialogClose asChild>
|
||||
<Button variant="tertiary/medium">Cancel</Button>
|
||||
</DialogClose>
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
{to && (
|
||||
<ChevronRightIcon className="size-5 text-charcoal-500 transition group-hover:translate-x-1 group-hover/banner:text-text-bright" />
|
||||
)}
|
||||
</a>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,14 +26,16 @@ function NextButton({ cursor }: { cursor?: string }) {
|
||||
<LinkButton
|
||||
to={path ?? "#"}
|
||||
variant={"minimal/small"}
|
||||
TrailingIcon="arrow-right"
|
||||
TrailingIcon="chevron-right"
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path &&
|
||||
"cursor-not-allowed opacity-50 group-hover:bg-transparent group-hover:text-text-dimmed"
|
||||
!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>
|
||||
@@ -47,14 +49,16 @@ function PreviousButton({ cursor }: { cursor?: string }) {
|
||||
<LinkButton
|
||||
to={path ?? "#"}
|
||||
variant={"minimal/small"}
|
||||
LeadingIcon="arrow-left"
|
||||
LeadingIcon="chevron-left"
|
||||
leadingIconClassName="text-text-dimmed"
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!path &&
|
||||
"cursor-not-allowed opacity-50 group-hover:bg-transparent group-hover:text-text-dimmed"
|
||||
!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>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export function FreePlanUsage({ to, percentage }: { to: string; percentage: numb
|
||||
<ArrowUpCircleIcon className="h-5 w-5 text-text-dimmed" />
|
||||
<Paragraph className="text-2sm text-text-bright">Free Plan</Paragraph>
|
||||
</div>
|
||||
<Link to={to} className="text-2sm text-text-link">
|
||||
<Link to={to} className="text-2sm text-text-link focus-custom">
|
||||
Upgrade
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -8,32 +8,16 @@ type UsageBarProps = {
|
||||
current: number;
|
||||
billingLimit?: number;
|
||||
tierLimit?: number;
|
||||
projectedUsage?: number;
|
||||
isPaying: boolean;
|
||||
};
|
||||
|
||||
const startFactor = 4;
|
||||
|
||||
export function UsageBar({
|
||||
current,
|
||||
billingLimit,
|
||||
tierLimit,
|
||||
projectedUsage,
|
||||
isPaying,
|
||||
}: UsageBarProps) {
|
||||
const getLargestNumber = Math.max(
|
||||
current,
|
||||
tierLimit ?? -Infinity,
|
||||
projectedUsage ?? -Infinity,
|
||||
billingLimit ?? -Infinity,
|
||||
5
|
||||
);
|
||||
export function UsageBar({ current, billingLimit, tierLimit, isPaying }: UsageBarProps) {
|
||||
const getLargestNumber = Math.max(current, tierLimit ?? -Infinity, billingLimit ?? -Infinity, 5);
|
||||
//creates a maximum range for the progress bar, add 10% to the largest number so the bar doesn't reach the end
|
||||
const maxRange = Math.round(getLargestNumber * 1.1);
|
||||
const tierRunLimitPercentage = tierLimit ? Math.round((tierLimit / maxRange) * 100) : 0;
|
||||
const projectedRunsPercentage = projectedUsage
|
||||
? Math.round((projectedUsage / maxRange) * 100)
|
||||
: 0;
|
||||
const billingLimitPercentage =
|
||||
billingLimit !== undefined ? Math.round((billingLimit / maxRange) * 100) : 0;
|
||||
const usagePercentage = Math.round((current / maxRange) * 100);
|
||||
@@ -42,7 +26,7 @@ export function UsageBar({
|
||||
const usageCappedToLimitPercentage = Math.min(usagePercentage, tierRunLimitPercentage);
|
||||
|
||||
return (
|
||||
<div className="h-fit w-full py-12">
|
||||
<div className="h-fit w-full py-6">
|
||||
<div className="relative h-3 w-full rounded-sm bg-background-bright">
|
||||
{billingLimit !== undefined && (
|
||||
<motion.div
|
||||
@@ -93,23 +77,6 @@ export function UsageBar({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{projectedUsage !== undefined && projectedUsage !== 0 && (
|
||||
<motion.div
|
||||
initial={{ width: projectedRunsPercentage / startFactor + "%" }}
|
||||
animate={{ width: projectedRunsPercentage + "%" }}
|
||||
transition={{ duration: 1.5, type: "spring" }}
|
||||
style={{ width: `${projectedRunsPercentage}%` }}
|
||||
className="absolute h-3 rounded-l-sm"
|
||||
>
|
||||
<Legend
|
||||
text="Projected:"
|
||||
value={formatCurrency(projectedUsage, false)}
|
||||
position="topRow2"
|
||||
percentage={projectedRunsPercentage}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
initial={{ width: usageCappedToLimitPercentage / startFactor + "%" }}
|
||||
animate={{ width: usageCappedToLimitPercentage + "%" }}
|
||||
|
||||
@@ -241,7 +241,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
onMouseEnter={() => setMouseOver(true)}
|
||||
onMouseLeave={() => setMouseOver(false)}
|
||||
className={cn(
|
||||
"absolute right-3 z-50 transition-colors duration-100 hover:cursor-pointer",
|
||||
"absolute right-3 z-50 transition-colors duration-100 focus-custom hover:cursor-pointer",
|
||||
showChrome ? "top-10" : "top-2.5",
|
||||
copied ? "text-emerald-500" : "text-charcoal-500 hover:text-charcoal-300"
|
||||
)}
|
||||
|
||||
@@ -112,9 +112,9 @@ export function JSONEditor(opts: JSONEditorProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
opts.className,
|
||||
"grid",
|
||||
showButtons ? "grid-rows-[2.5rem_1fr]" : "grid-rows-[1fr]"
|
||||
showButtons ? "grid-rows-[2.5rem_1fr]" : "grid-rows-[1fr]",
|
||||
opts.className
|
||||
)}
|
||||
>
|
||||
{showButtons && (
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/20/solid";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { generateTwoRandomWords } from "~/utils/randomWords";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { Fieldset } from "../primitives/Fieldset";
|
||||
import { InputGroup } from "../primitives/InputGroup";
|
||||
import { FormButtons } from "../primitives/FormButtons";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { InputGroup } from "../primitives/InputGroup";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
|
||||
type ModalProps = {
|
||||
id: string;
|
||||
@@ -31,15 +29,17 @@ export function RegenerateApiKeyModal({ id, title }: ModalProps) {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="minimal/small"
|
||||
leadingIconClassName="text-text-dimmed"
|
||||
variant="small-menu-item"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
leadingIconClassName="text-success"
|
||||
LeadingIcon={ArrowPathIcon}
|
||||
>
|
||||
Regenerate
|
||||
Regenerate…
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>{`Regenerate ${title} Environment Key`}</DialogHeader>
|
||||
<DialogHeader>{`Regenerate ${title.toUpperCase()} environment key`}</DialogHeader>
|
||||
<RegenerateApiKeyModalContent
|
||||
id={id}
|
||||
title={title}
|
||||
@@ -62,10 +62,10 @@ const RegenerateApiKeyModalContent = ({ id, randomWord, title, closeModal }: Mod
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-y-5 py-4">
|
||||
<div className="flex flex-col items-center gap-y-4 pt-4">
|
||||
<Callout variant="warning">
|
||||
{`Regenerating the keys for this environment will temporarily break any live Jobs in the
|
||||
${title} Environmentuntil the new API keys are set in the relevant environment variables.`}
|
||||
{`Regenerating the keys for this environment will temporarily break any live tasks in the
|
||||
${title} environment until the new API keys are set in the relevant environment variables.`}
|
||||
</Callout>
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
@@ -73,7 +73,7 @@ const RegenerateApiKeyModalContent = ({ id, randomWord, title, closeModal }: Mod
|
||||
className="mt-2 w-full"
|
||||
>
|
||||
<Fieldset className="w-full">
|
||||
<InputGroup>
|
||||
<InputGroup className="max-w-full">
|
||||
<Paragraph variant="small/bright">Enter this text below to confirm:</Paragraph>
|
||||
<Paragraph
|
||||
variant="small"
|
||||
@@ -93,14 +93,14 @@ const RegenerateApiKeyModalContent = ({ id, randomWord, title, closeModal }: Mod
|
||||
confirmButton={
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"primary/small"}
|
||||
variant={"primary/medium"}
|
||||
LeadingIcon={isSubmitting ? Spinner : undefined}
|
||||
disabled={confirmationText !== randomWord}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
}
|
||||
cancelButton={<Button variant={"tertiary/small"}>Cancel</Button>}
|
||||
cancelButton={<Button variant={"tertiary/medium"}>Cancel</Button>}
|
||||
/>
|
||||
</Fieldset>
|
||||
</fetcher.Form>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
|
||||
import { DiscordIcon } from "@trigger.dev/companyicons";
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { accountPath, personalAccessTokensPath, rootPath } from "~/utils/pathBuilder";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
|
||||
|
||||
export function AccountSideMenu({ user }: { user: User }) {
|
||||
const { v3Enabled } = useFeatures();
|
||||
@@ -20,7 +19,7 @@ export function AccountSideMenu({ user }: { user: User }) {
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className={cn("flex items-center justify-between border-b p-px transition")}>
|
||||
<div className={cn("flex h-10 items-center justify-between border-b p-1 transition")}>
|
||||
<LinkButton
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={ArrowLeftIcon}
|
||||
@@ -38,7 +37,7 @@ export function AccountSideMenu({ user }: { user: User }) {
|
||||
<SideMenuItem
|
||||
name="Your profile"
|
||||
icon="account"
|
||||
iconColor="text-indigo-500"
|
||||
activeIconColor="text-indigo-500"
|
||||
to={accountPath()}
|
||||
data-action="account"
|
||||
/>
|
||||
@@ -49,7 +48,7 @@ export function AccountSideMenu({ user }: { user: User }) {
|
||||
<SideMenuItem
|
||||
name="Personal Access Tokens"
|
||||
icon={ShieldCheckIcon}
|
||||
iconColor="text-emerald-500"
|
||||
activeIconColor="text-emerald-500"
|
||||
to={personalAccessTokensPath()}
|
||||
data-action="tokens"
|
||||
/>
|
||||
@@ -57,42 +56,7 @@ export function AccountSideMenu({ user }: { user: User }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 border-t border-grid-bright p-1">
|
||||
<SideMenuItem
|
||||
name="Join our Discord"
|
||||
icon={DiscordIcon}
|
||||
to="https://trigger.dev/discord"
|
||||
data-action="join our discord"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<SideMenuItem
|
||||
name="Documentation"
|
||||
icon="docs"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="star"
|
||||
to="https://trigger.dev/changelog"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon="log"
|
||||
data-action="help & feedback"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Help & Feedback
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<HelpAndFeedback />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
CalendarDaysIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
EnvelopeIcon,
|
||||
LightBulbIcon,
|
||||
SignalIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { StepContentContainer } from "../StepContentContainer";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
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();
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={(open) => setHelpMenuOpen(open)}>
|
||||
<PopoverSideMenuTrigger isOpen={isHelpMenuOpen} shortcut={{ key: "h" }}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ChatBubbleLeftEllipsisIcon className="size-4 text-success" />
|
||||
Help & Feedback
|
||||
</div>
|
||||
</PopoverSideMenuTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-[14rem] divide-y divide-grid-bright overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="start"
|
||||
>
|
||||
<Fragment>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Documentation"
|
||||
icon="docs"
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-green-500"
|
||||
activeIconColor="text-green-500"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Status"
|
||||
icon={SignalIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-green-500"
|
||||
activeIconColor="text-green-500"
|
||||
to="https://status.trigger.dev/"
|
||||
data-action="status"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Suggest a feature"
|
||||
icon={LightBulbIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-sun-500"
|
||||
activeIconColor="text-sun-500"
|
||||
to="https://feedback.trigger.dev/"
|
||||
data-action="suggest-a-feature"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="star"
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-sun-500"
|
||||
activeIconColor="text-sun-500"
|
||||
to="https://trigger.dev/changelog"
|
||||
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>
|
||||
{currentPlan?.v3Subscription?.plan?.limits.support === "slack" && (
|
||||
<div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={SlackIcon}
|
||||
data-action="join-our-slack"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="text-text-bright">Join our Slack…</span>
|
||||
<MenuCount count="PRO" />
|
||||
</div>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>Join our Slack</DialogHeader>
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Icon icon={SlackIcon} className="h-10 w-10 min-w-[2.5rem]" />
|
||||
<Paragraph variant="base/bright">
|
||||
As a subscriber, you have access to a dedicated Slack channel for 1-to-1
|
||||
support with the Trigger.dev team.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<hr className="border-charcoal-800" />
|
||||
<div>
|
||||
<StepNumber stepNumber="1" title="Email us" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Send us an email to this address from your Trigger.dev account email
|
||||
address:
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value="priority-support@trigger.dev"
|
||||
className="my-2"
|
||||
/>
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Look out for an invite from Slack" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
As soon as we can, we'll setup a Slack Connect channel and say hello!
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)}
|
||||
<SideMenuItem
|
||||
name="Ask in our Discord"
|
||||
icon={DiscordIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://trigger.dev/discord"
|
||||
data-action="join our discord"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Book a 15 min call"
|
||||
icon={CalendarDaysIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-rose-500"
|
||||
activeIconColor="text-rose-500"
|
||||
to="https://cal.com/team/triggerdotdev/founders-call"
|
||||
data-action="book-a-call"
|
||||
target="_blank"
|
||||
/>
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={EnvelopeIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
data-action="contact-us"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Contact us…
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -12,18 +12,18 @@ import {
|
||||
RectangleStackIcon,
|
||||
ServerStackIcon,
|
||||
ShieldCheckIcon,
|
||||
SignalIcon,
|
||||
Squares2X2Icon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { DiscordIcon, SlackIcon } from "@trigger.dev/companyicons";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { type MatchedOrganization } from "~/hooks/useOrganizations";
|
||||
import { type MatchedProject } from "~/hooks/useProject";
|
||||
import { type User } from "~/models/user.server";
|
||||
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { FeedbackType } from "~/routes/resources.feedback";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
accountPath,
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
projectSetupPath,
|
||||
projectTriggersPath,
|
||||
v3ApiKeysPath,
|
||||
v3BatchesPath,
|
||||
v3BillingPath,
|
||||
v3ConcurrencyPath,
|
||||
v3DeploymentsPath,
|
||||
@@ -58,17 +59,13 @@ import {
|
||||
v3TestPath,
|
||||
v3UsagePath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { Feedback } from "../Feedback";
|
||||
import { ImpersonationBanner } from "../ImpersonationBanner";
|
||||
import { LogoIcon } from "../LogoIcon";
|
||||
import { StepContentContainer } from "../StepContentContainer";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { FreePlanUsage } from "../billing/v2/FreePlanUsage";
|
||||
import { Badge } from "../primitives/Badge";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
Popover,
|
||||
@@ -78,8 +75,8 @@ import {
|
||||
PopoverMenuItem,
|
||||
PopoverSectionHeader,
|
||||
} from "../primitives/Popover";
|
||||
import { StepNumber } from "../primitives/StepNumber";
|
||||
import { TextLink } from "../primitives/TextLink";
|
||||
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
|
||||
import { SideMenuHeader } from "./SideMenuHeader";
|
||||
import { SideMenuItem } from "./SideMenuItem";
|
||||
|
||||
@@ -91,8 +88,38 @@ type SideMenuProps = {
|
||||
project: SideMenuProject;
|
||||
organization: MatchedOrganization;
|
||||
organizations: MatchedOrganization[];
|
||||
button?: ReactNode;
|
||||
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);
|
||||
@@ -171,7 +198,7 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
name="Team"
|
||||
icon={UserGroupIcon}
|
||||
to={organizationTeamPath(organization)}
|
||||
iconColor="text-amber-500"
|
||||
activeIconColor="text-amber-500"
|
||||
data-action="team"
|
||||
/>
|
||||
{organization.projects.some((proj) => proj.version === "V3") && isManagedCloud && (
|
||||
@@ -180,14 +207,14 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
name="Usage"
|
||||
icon={ChartBarIcon}
|
||||
to={v3UsagePath(organization)}
|
||||
iconColor="text-green-600"
|
||||
activeIconColor="text-green-600"
|
||||
data-action="usage"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Billing"
|
||||
icon={CreditCardIcon}
|
||||
to={v3BillingPath(organization)}
|
||||
iconColor="text-blue-600"
|
||||
activeIconColor="text-blue-600"
|
||||
data-action="billing"
|
||||
badge={
|
||||
currentPlan?.v3Subscription?.isPaying
|
||||
@@ -202,14 +229,14 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
name="Usage (v2)"
|
||||
icon={ChartBarIcon}
|
||||
to={organizationBillingPath(organization)}
|
||||
iconColor="text-green-600"
|
||||
activeIconColor="text-green-600"
|
||||
data-action="usage & billing"
|
||||
/>
|
||||
)}
|
||||
<SideMenuItem
|
||||
name="Organization settings"
|
||||
icon="settings"
|
||||
iconColor="text-teal-500"
|
||||
activeIconColor="text-teal-500"
|
||||
to={organizationSettingsPath(organization)}
|
||||
data-action="organization-settings"
|
||||
/>
|
||||
@@ -217,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
|
||||
@@ -239,96 +267,7 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 border-t border-grid-bright p-1">
|
||||
{currentPlan?.v3Subscription?.plan?.limits.support === "slack" && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon={SlackIcon}
|
||||
data-action="join our slack"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Join our Slack
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>Join our Slack</DialogHeader>
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Icon icon={SlackIcon} className="h-10 w-10 min-w-[2.5rem]" />
|
||||
<Paragraph variant="base/bright">
|
||||
As a subscriber, you have access to a dedicated Slack channel for 1-to-1
|
||||
support with the Trigger.dev team.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<hr className="border-charcoal-800" />
|
||||
<div>
|
||||
<StepNumber stepNumber="1" title="Email us" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Send us an email to this address from your Trigger.dev account email
|
||||
address:
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
value="priority-support@trigger.dev"
|
||||
className="my-2"
|
||||
/>
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Look out for an invite from Slack" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
As soon as we can, we'll setup a Slack Connect channel and say hello!
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
<SideMenuItem
|
||||
name="Join our Discord"
|
||||
icon={DiscordIcon}
|
||||
to="https://trigger.dev/discord"
|
||||
data-action="join our discord"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Documentation"
|
||||
icon="docs"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Changelog"
|
||||
icon="star"
|
||||
to="https://trigger.dev/changelog"
|
||||
data-action="changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Status"
|
||||
icon={SignalIcon}
|
||||
to="https://status.trigger.dev/"
|
||||
data-action="status"
|
||||
target="_blank"
|
||||
/>
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
LeadingIcon="log"
|
||||
leadingIconClassName="text-primary"
|
||||
data-action="help & feedback"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
<span className="text-primary">Help & Feedback</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<HelpAndFeedback />
|
||||
{isV3Project && isFreeV3User && (
|
||||
<FreePlanUsage
|
||||
to={v3BillingPath(organization)}
|
||||
@@ -495,47 +434,47 @@ function V2ProjectSideMenu({
|
||||
<SideMenuItem
|
||||
name="Jobs"
|
||||
icon="job"
|
||||
iconColor="text-indigo-500"
|
||||
activeIconColor="text-indigo-500"
|
||||
to={projectPath(organization, project)}
|
||||
data-action="jobs"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Runs"
|
||||
icon="runs"
|
||||
iconColor="text-teal-500"
|
||||
activeIconColor="text-teal-500"
|
||||
to={projectRunsPath(organization, project)}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Triggers"
|
||||
icon="trigger"
|
||||
iconColor="text-amber-500"
|
||||
activeIconColor="text-amber-500"
|
||||
to={projectTriggersPath(organization, project)}
|
||||
data-action="triggers"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Events"
|
||||
icon={CursorArrowRaysIcon}
|
||||
iconColor="text-sky-500"
|
||||
activeIconColor="text-sky-500"
|
||||
to={projectEventsPath(organization, project)}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="HTTP endpoints"
|
||||
icon="http-endpoint"
|
||||
iconColor="text-pink-500"
|
||||
activeIconColor="text-pink-500"
|
||||
to={projectHttpEndpointsPath(organization, project)}
|
||||
data-action="httpendpoints"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Environments & API Keys"
|
||||
icon="environment"
|
||||
iconColor="text-rose-500"
|
||||
activeIconColor="text-rose-500"
|
||||
to={projectEnvironmentsPath(organization, project)}
|
||||
data-action="environments & api keys"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Project settings"
|
||||
icon="settings"
|
||||
iconColor="text-teal-500"
|
||||
activeIconColor="text-teal-500"
|
||||
to={projectSettingsPath(organization, project)}
|
||||
data-action="project-settings"
|
||||
/>
|
||||
@@ -550,49 +489,54 @@ function V3ProjectSideMenu({
|
||||
project: SideMenuProject;
|
||||
organization: MatchedOrganization;
|
||||
}) {
|
||||
const { alertsEnabled } = useFeatures();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideMenuHeader title={"Project"} />
|
||||
<SideMenuItem
|
||||
name="Tasks"
|
||||
icon={TaskIcon}
|
||||
iconColor="text-blue-500"
|
||||
activeIconColor="text-blue-500"
|
||||
to={v3ProjectPath(organization, project)}
|
||||
data-action="tasks"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Runs"
|
||||
icon="runs"
|
||||
iconColor="text-teal-500"
|
||||
activeIconColor="text-teal-500"
|
||||
to={v3RunsPath(organization, project)}
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Batches"
|
||||
icon={Squares2X2Icon}
|
||||
activeIconColor="text-blue-500"
|
||||
to={v3BatchesPath(organization, project)}
|
||||
data-action="batches"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Test"
|
||||
icon={BeakerIcon}
|
||||
iconColor="text-lime-500"
|
||||
activeIconColor="text-lime-500"
|
||||
to={v3TestPath(organization, project)}
|
||||
data-action="test"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Schedules"
|
||||
icon={ClockIcon}
|
||||
iconColor="text-sun-500"
|
||||
activeIconColor="text-sun-500"
|
||||
to={v3SchedulesPath(organization, project)}
|
||||
data-action="schedules"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="API keys"
|
||||
icon={KeyIcon}
|
||||
iconColor="text-amber-500"
|
||||
activeIconColor="text-amber-500"
|
||||
to={v3ApiKeysPath(organization, project)}
|
||||
data-action="api keys"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Environment variables"
|
||||
icon={IdentificationIcon}
|
||||
iconColor="text-pink-500"
|
||||
activeIconColor="text-pink-500"
|
||||
to={v3EnvironmentVariablesPath(organization, project)}
|
||||
data-action="environment variables"
|
||||
/>
|
||||
@@ -600,30 +544,28 @@ function V3ProjectSideMenu({
|
||||
<SideMenuItem
|
||||
name="Deployments"
|
||||
icon={ServerStackIcon}
|
||||
iconColor="text-blue-500"
|
||||
activeIconColor="text-blue-500"
|
||||
to={v3DeploymentsPath(organization, project)}
|
||||
data-action="deployments"
|
||||
/>
|
||||
{alertsEnabled && (
|
||||
<SideMenuItem
|
||||
name="Alerts"
|
||||
icon={BellAlertIcon}
|
||||
iconColor="text-red-500"
|
||||
to={v3ProjectAlertsPath(organization, project)}
|
||||
data-action="alerts"
|
||||
/>
|
||||
)}
|
||||
<SideMenuItem
|
||||
name="Alerts"
|
||||
icon={BellAlertIcon}
|
||||
activeIconColor="text-red-500"
|
||||
to={v3ProjectAlertsPath(organization, project)}
|
||||
data-action="alerts"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Concurrency limits"
|
||||
icon={RectangleStackIcon}
|
||||
iconColor="text-indigo-500"
|
||||
activeIconColor="text-indigo-500"
|
||||
to={v3ConcurrencyPath(organization, project)}
|
||||
data-action="concurrency"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Project settings"
|
||||
icon="settings"
|
||||
iconColor="text-teal-500"
|
||||
activeIconColor="text-teal-500"
|
||||
to={v3ProjectSettingsPath(organization, project)}
|
||||
data-action="project-settings"
|
||||
/>
|
||||
|
||||
@@ -3,25 +3,26 @@ import { usePathName } from "~/hooks/usePathName";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { LinkButton } from "../primitives/Buttons";
|
||||
import { type IconNames } from "../primitives/NamedIcon";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { Icon } from "../primitives/Icon";
|
||||
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||
|
||||
export function SideMenuItem({
|
||||
icon,
|
||||
iconColor,
|
||||
activeIconColor,
|
||||
inactiveIconColor,
|
||||
trailingIcon,
|
||||
trailingIconClassName,
|
||||
name,
|
||||
to,
|
||||
hasWarning,
|
||||
badge,
|
||||
target,
|
||||
subItem = false,
|
||||
}: {
|
||||
icon?: IconNames | React.ComponentType<any>;
|
||||
iconColor?: string;
|
||||
activeIconColor?: string;
|
||||
inactiveIconColor?: string;
|
||||
trailingIcon?: IconNames | React.ComponentType<any>;
|
||||
trailingIconClassName?: string;
|
||||
name: string;
|
||||
to: string;
|
||||
hasWarning?: string | boolean;
|
||||
badge?: string;
|
||||
target?: AnchorHTMLAttributes<HTMLAnchorElement>["target"];
|
||||
subItem?: boolean;
|
||||
@@ -35,11 +36,13 @@ export function SideMenuItem({
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
LeadingIcon={icon}
|
||||
leadingIconClassName={isActive ? iconColor : "text-text-dimmed"}
|
||||
leadingIconClassName={isActive ? activeIconColor : inactiveIconColor ?? "text-text-dimmed"}
|
||||
TrailingIcon={trailingIcon}
|
||||
trailingIconClassName={trailingIconClassName}
|
||||
to={to}
|
||||
target={target}
|
||||
className={cn(
|
||||
"text-text-bright group-hover:bg-charcoal-750",
|
||||
"text-text-bright group-hover:bg-charcoal-750 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
subItem ? "text-text-dimmed" : "",
|
||||
isActive ? "bg-tertiary text-text-bright" : "group-hover:text-text-bright"
|
||||
)}
|
||||
@@ -48,27 +51,13 @@ export function SideMenuItem({
|
||||
{name}
|
||||
<div className="flex items-center gap-1">
|
||||
{badge !== undefined && <MenuCount count={badge} />}
|
||||
{typeof hasWarning === "string" ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Icon icon={IconExclamationCircle} className="h-5 w-5 text-error" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-1 border border-error bg-error/20 backdrop-blur-xl">
|
||||
{hasWarning}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
hasWarning && <Icon icon={IconExclamationCircle} className="h-5 w-5 text-error" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuCount({ count }: { count: number | string }) {
|
||||
export function MenuCount({ count }: { count: number | string }) {
|
||||
return (
|
||||
<div className="rounded-full bg-charcoal-900 px-2 py-1 text-xxs uppercase tracking-wider text-text-dimmed">
|
||||
{count}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variants = {
|
||||
small: {
|
||||
size: "size-[1rem]",
|
||||
arrowHeadRight: "group-hover:translate-x-[3px]",
|
||||
arrowLineRight: "h-[1.5px] w-[7px] translate-x-1 top-[calc(50%-0.5px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-[3px]",
|
||||
arrowLineLeft: "h-[1.5px] w-[7px] translate-x-1 top-[calc(50%-0.5px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
medium: {
|
||||
size: "size-[1.1rem]",
|
||||
arrowHeadRight: "group-hover:translate-x-[3px]",
|
||||
arrowLineRight: "h-[1.5px] w-[9px] translate-x-1 top-[calc(50%-1px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-[-3px]",
|
||||
arrowLineLeft: "h-[1.5px] w-[9px] translate-x-1 top-[calc(50%-1px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
large: {
|
||||
size: "size-6",
|
||||
arrowHeadRight: "group-hover:translate-x-1",
|
||||
arrowLineRight: "h-[2.3px] w-[12px] translate-x-[6px] top-[calc(50%-1px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-1",
|
||||
arrowLineLeft: "h-[2.3px] w-[12px] translate-x-[6px] top-[calc(50%-1px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
"extra-large": {
|
||||
size: "size-8",
|
||||
arrowHeadRight: "group-hover:translate-x-1",
|
||||
arrowLineRight: "h-[3px] w-[16px] translate-x-[8px] top-[calc(50%-1.5px)]",
|
||||
arrowHeadLeft: "group-hover:translate-x-1",
|
||||
arrowLineLeft: "h-[3px] w-[16px] translate-x-[8px] top-[calc(50%-1.5px)]",
|
||||
arrowHeadTopRight:
|
||||
"-translate-x-0 transition group-hover:translate-x-[3px] group-hover:translate-y-[-3px]",
|
||||
},
|
||||
};
|
||||
|
||||
export const themes = {
|
||||
dark: {
|
||||
textStyle: "text-background-bright",
|
||||
arrowLine: "bg-background-bright",
|
||||
},
|
||||
dimmed: {
|
||||
textStyle: "text-text-dimmed",
|
||||
arrowLine: "bg-text-dimmed",
|
||||
},
|
||||
bright: {
|
||||
textStyle: "text-text-bright",
|
||||
arrowLine: "bg-text-bright",
|
||||
},
|
||||
primary: {
|
||||
textStyle: "text-text-dimmed group-hover:text-primary",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-primary",
|
||||
},
|
||||
blue: {
|
||||
textStyle: "text-text-dimmed group-hover:text-blue-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-blue-500",
|
||||
},
|
||||
rose: {
|
||||
textStyle: "text-text-dimmed group-hover:text-rose-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-rose-500",
|
||||
},
|
||||
amber: {
|
||||
textStyle: "text-text-dimmed group-hover:text-amber-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-amber-500",
|
||||
},
|
||||
apple: {
|
||||
textStyle: "text-text-dimmed group-hover:text-apple-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-apple-500",
|
||||
},
|
||||
lavender: {
|
||||
textStyle: "text-text-dimmed group-hover:text-lavender-500",
|
||||
arrowLine: "bg-text-dimmed group-hover:bg-lavender-500",
|
||||
},
|
||||
};
|
||||
|
||||
type Variants = keyof typeof variants;
|
||||
type Theme = keyof typeof themes;
|
||||
|
||||
type AnimatingArrowProps = {
|
||||
className?: string;
|
||||
variant?: Variants;
|
||||
theme?: Theme;
|
||||
direction?: "right" | "left" | "topRight";
|
||||
};
|
||||
|
||||
export function AnimatingArrow({
|
||||
className,
|
||||
variant = "medium",
|
||||
theme = "dimmed",
|
||||
direction = "right",
|
||||
}: AnimatingArrowProps) {
|
||||
const variantStyles = variants[variant];
|
||||
const themeStyles = themes[theme];
|
||||
|
||||
return (
|
||||
<span className={cn("relative -mr-1 ml-1 flex", variantStyles.size, className)}>
|
||||
{direction === "topRight" && (
|
||||
<>
|
||||
<svg
|
||||
className={cn(
|
||||
"absolute top-[5px] transition duration-200 ease-in-out",
|
||||
themeStyles.textStyle
|
||||
)}
|
||||
width="9"
|
||||
height="8"
|
||||
viewBox="0 0 9 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1.5 7L7.5 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
className={cn(
|
||||
"absolute top-[5px] transition duration-300 ease-in-out",
|
||||
themeStyles.textStyle,
|
||||
variantStyles.arrowHeadTopRight
|
||||
)}
|
||||
width="9"
|
||||
height="8"
|
||||
viewBox="0 0 9 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 1H7.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M7.5 7L7.5 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M1 7.5L7.5 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
{direction === "right" && (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute rounded-full opacity-0 transition duration-300 ease-in-out group-hover:opacity-100",
|
||||
variantStyles.arrowLineRight,
|
||||
themeStyles.arrowLine
|
||||
)}
|
||||
/>
|
||||
<ChevronRightIcon
|
||||
className={cn(
|
||||
"absolute -translate-x-0.5 transition duration-300 ease-in-out",
|
||||
variantStyles.arrowHeadRight,
|
||||
variantStyles.size,
|
||||
themeStyles.textStyle
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{direction === "left" && (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute rounded-full opacity-0 transition duration-300 ease-in-out group-hover:opacity-100",
|
||||
variantStyles.arrowLineLeft,
|
||||
themeStyles.arrowLine
|
||||
)}
|
||||
/>
|
||||
<ChevronLeftIcon
|
||||
className={cn(
|
||||
"absolute translate-x-0.5 transition duration-300 ease-in-out",
|
||||
variantStyles.arrowHeadLeft,
|
||||
variantStyles.size,
|
||||
themeStyles.textStyle
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,10 @@ export function AppliedFilter({
|
||||
</div>
|
||||
{removable && (
|
||||
<button
|
||||
className={cn("group flex size-6 items-center justify-center", variantClassName.clear)}
|
||||
className={cn(
|
||||
"group flex size-6 items-center justify-center focus-custom",
|
||||
variantClassName.clear
|
||||
)}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<XMarkIcon className="size-3.5" />
|
||||
|
||||
@@ -6,6 +6,8 @@ const variants = {
|
||||
"grid place-items-center rounded-full px-2 h-5 tracking-wider text-xxs bg-charcoal-750 text-text-bright uppercase whitespace-nowrap",
|
||||
small:
|
||||
"grid place-items-center rounded-full px-[0.4rem] h-4 tracking-wider text-xxs bg-background-dimmed text-text-dimmed uppercase whitespace-nowrap",
|
||||
"extra-small":
|
||||
"grid place-items-center border border-charcoal-650 rounded-sm px-1 h-4 border-tracking-wider text-xxs bg-background-bright text-blue-500 whitespace-nowrap",
|
||||
outline:
|
||||
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider text-xxs border border-dimmed text-text-dimmed uppercase whitespace-nowrap",
|
||||
"outline-rounded":
|
||||
|
||||
@@ -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: {
|
||||
@@ -41,40 +42,52 @@ type Size = keyof typeof sizes;
|
||||
const theme = {
|
||||
primary: {
|
||||
textColor:
|
||||
"text-charcoal-900 group-hover:text-charcoal-900 transition group-disabled:text-charcoal-900",
|
||||
"text-charcoal-900 group-hover/button:text-charcoal-900 transition group-disabled/button:text-charcoal-900",
|
||||
button:
|
||||
"bg-primary group-hover:bg-apple-200 group-disabled:opacity-50 group-disabled:bg-primary group-disabled:pointer-events-none",
|
||||
"bg-primary group-hover/button:bg-apple-200 group-disabled/button:opacity-50 group-disabled/button:bg-primary group-disabled/button:pointer-events-none",
|
||||
shortcut:
|
||||
"border-black/40 text-charcoal-900 group-hover:border-black/60 group-hover:text-charcoal-900",
|
||||
"border-black/40 text-charcoal-900 group-hover/button:border-black/60 group-hover/button:text-charcoal-900",
|
||||
icon: "text-charcoal-900",
|
||||
},
|
||||
secondary: {
|
||||
textColor: "text-secondary group-hover:text-secondary transition group-disabled:text-secondary",
|
||||
textColor: "text-text-bright transition group-disabled/button:text-text-dimmed/80",
|
||||
button:
|
||||
"bg-transparent border border-secondary group-hover:border-secondary group-hover:bg-secondary/10 group-disabled:opacity-30 group-disabled:border-secondary group-disabled:bg-transparent group-disabled:pointer-events-none",
|
||||
"bg-secondary group-hover/button:bg-charcoal-600 group-hover/button:border-charcoal-650 border border-charcoal-600 group-disabled/button:bg-secondary group-disabled/button:opacity-60 group-disabled/button:pointer-events-none",
|
||||
shortcut:
|
||||
"border-secondary/30 text-secondary group-hover:text-text-bright/80 group-hover:border-dimmed/60",
|
||||
"border-text-dimmed/40 text-text-dimmed group-hover/button:text-text-bright group-hover/button:border-text-dimmed",
|
||||
icon: "text-text-bright",
|
||||
},
|
||||
tertiary: {
|
||||
textColor: "text-text-bright transition group-disabled:text-text-dimmed/80",
|
||||
textColor: "text-text-bright transition group-disabled/button:text-text-dimmed/80",
|
||||
button:
|
||||
"bg-tertiary group-hover:bg-charcoal-600 group-disabled:bg-tertiary group-disabled:opacity-60 group-disabled:pointer-events-none",
|
||||
"bg-tertiary group-hover/button:bg-charcoal-600 group-disabled/button:bg-tertiary group-disabled/button:opacity-60 group-disabled/button:pointer-events-none",
|
||||
shortcut:
|
||||
"border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright group-hover:border-text-dimmed",
|
||||
"border-text-dimmed/40 text-text-dimmed group-hover/button:text-text-bright group-hover/button:border-text-dimmed",
|
||||
icon: "text-text-bright",
|
||||
},
|
||||
minimal: {
|
||||
textColor:
|
||||
"text-text-dimmed group-hover:text-text-bright transition group-disabled:text-text-dimmed/80",
|
||||
textColor: "text-text-dimmed group-disabled/button:text-text-dimmed transition",
|
||||
button:
|
||||
"bg-transparent group-hover:bg-tertiary disabled:opacity-50 group-disabled:bg-transparent group-disabled:pointer-events-none",
|
||||
"bg-transparent group-hover/button:bg-tertiary disabled:opacity-50 group-disabled/button:bg-transparent group-disabled/button:pointer-events-none",
|
||||
shortcut:
|
||||
"border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60",
|
||||
"border-dimmed/40 text-text-dimmed group-hover/button:text-text-bright/80 group-hover/button:border-dimmed/60",
|
||||
icon: "text-text-dimmed",
|
||||
},
|
||||
danger: {
|
||||
textColor:
|
||||
"text-text-bright group-hover:text-white transition group-disabled:text-text-bright/80",
|
||||
"text-text-bright group-hover/button:text-white transition group-disabled/button:text-text-bright/80",
|
||||
button:
|
||||
"bg-error group-hover:bg-rose-500 disabled:opacity-50 group-disabled:bg-error group-disabled:pointer-events-none",
|
||||
shortcut: "border-text-bright text-text-bright group-hover:border-bright/60",
|
||||
"bg-error group-hover/button:bg-rose-500 disabled:opacity-50 group-disabled/button:bg-error group-disabled/button:pointer-events-none",
|
||||
shortcut: "border-text-bright text-text-bright group-hover/button:border-bright/60",
|
||||
icon: "text-text-bright",
|
||||
},
|
||||
docs: {
|
||||
textColor: "text-blue-200/70 transition group-disabled/button:text-text-dimmed/80",
|
||||
button:
|
||||
"bg-charcoal-700 border border-charcoal-600/50 shadow group-hover/button:bg-charcoal-650 group-disabled/button:bg-tertiary group-disabled/button:opacity-60 group-disabled/button:pointer-events-none",
|
||||
shortcut:
|
||||
"border-text-dimmed/40 text-text-dimmed group-hover/button:text-text-bright group-hover/button:border-text-dimmed",
|
||||
icon: "text-blue-500",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,7 +97,7 @@ function createVariant(sizeName: Size, themeName: Theme) {
|
||||
return {
|
||||
textColor: theme[themeName].textColor,
|
||||
button: cn(sizes[sizeName].button, theme[themeName].button),
|
||||
icon: sizes[sizeName].icon,
|
||||
icon: cn(sizes[sizeName].icon, theme[themeName].icon),
|
||||
iconSpacing: sizes[sizeName].iconSpacing,
|
||||
shortcutVariant: sizes[sizeName].shortcutVariant,
|
||||
shortcut: cn(sizes[sizeName].shortcut, theme[themeName].shortcut),
|
||||
@@ -112,10 +125,14 @@ const variant = {
|
||||
"danger/medium": createVariant("medium", "danger"),
|
||||
"danger/large": createVariant("large", "danger"),
|
||||
"danger/extra-large": createVariant("extra-large", "danger"),
|
||||
"docs/small": createVariant("small", "docs"),
|
||||
"docs/medium": createVariant("medium", "docs"),
|
||||
"docs/large": createVariant("large", "docs"),
|
||||
"docs/extra-large": createVariant("extra-large", "docs"),
|
||||
"menu-item": {
|
||||
textColor: "text-text-bright px-1",
|
||||
button:
|
||||
"h-9 px-[0.475rem] text-sm rounded-sm bg-transparent group-hover:bg-charcoal-800 transition",
|
||||
"h-9 px-[0.475rem] text-sm rounded-sm bg-transparent group-hover/button:bg-charcoal-750",
|
||||
icon: "h-5",
|
||||
iconSpacing: "gap-x-0.5",
|
||||
shortcutVariant: undefined,
|
||||
@@ -124,7 +141,7 @@ const variant = {
|
||||
"small-menu-item": {
|
||||
textColor: "text-text-bright",
|
||||
button:
|
||||
"h-[1.8rem] px-[0.4rem] text-2sm rounded-sm text-text-dimmed bg-transparent group-hover:bg-charcoal-850 transition",
|
||||
"h-[1.8rem] px-[0.4rem] text-2sm rounded-sm text-text-dimmed bg-transparent group-hover/button:bg-charcoal-750",
|
||||
icon: "h-4",
|
||||
iconSpacing: "gap-x-1.5",
|
||||
shortcutVariant: undefined,
|
||||
@@ -133,7 +150,7 @@ const variant = {
|
||||
"small-menu-sub-item": {
|
||||
textColor: "text-text-dimmed",
|
||||
button:
|
||||
"h-[1.8rem] px-[0.5rem] ml-5 text-2sm rounded-sm text-text-dimmed bg-transparent group-hover:bg-charcoal-850 transition",
|
||||
"h-[1.8rem] px-[0.5rem] ml-5 text-2sm rounded-sm text-text-dimmed bg-transparent group-hover/button:bg-charcoal-750 focus-custom",
|
||||
icon: undefined,
|
||||
iconSpacing: undefined,
|
||||
shortcutVariant: undefined,
|
||||
@@ -142,7 +159,7 @@ const variant = {
|
||||
};
|
||||
|
||||
const allVariants = {
|
||||
$all: "font-normal text-center font-sans justify-center items-center shrink-0 transition duration-150 rounded-[3px] select-none group-focus:outline-none group-disabled:opacity-75 group-disabled:pointer-events-none",
|
||||
$all: "font-normal text-center font-sans justify-center items-center shrink-0 transition duration-150 rounded-[3px] select-none group-focus/button:outline-none group-disabled/button:opacity-75 group-disabled/button:pointer-events-none focus-custom",
|
||||
variant: variant,
|
||||
};
|
||||
|
||||
@@ -157,6 +174,9 @@ export type ButtonContentPropsType = {
|
||||
className?: string;
|
||||
shortcut?: ShortcutDefinition;
|
||||
variant: keyof typeof variant;
|
||||
shortcutPosition?: "before-trailing-icon" | "after-trailing-icon";
|
||||
tooltip?: ReactNode;
|
||||
iconSpacing?: string;
|
||||
};
|
||||
|
||||
export function ButtonContent(props: ButtonContentPropsType) {
|
||||
@@ -170,36 +190,52 @@ 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 &&
|
||||
(typeof LeadingIcon === "string" ? (
|
||||
<NamedIcon
|
||||
name={LeadingIcon}
|
||||
className={cn(iconClassName, leadingIconClassName, "shrink-0 justify-start")}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
leadingIconClassName,
|
||||
"shrink-0 justify-start",
|
||||
variation.icon
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<LeadingIcon
|
||||
className={cn(
|
||||
iconClassName,
|
||||
textColorClassName,
|
||||
variation.icon,
|
||||
leadingIconClassName,
|
||||
"shrink-0 justify-start"
|
||||
)}
|
||||
@@ -215,32 +251,55 @@ export function ButtonContent(props: ButtonContentPropsType) {
|
||||
<>{text}</>
|
||||
))}
|
||||
|
||||
{shortcut &&
|
||||
!tooltip &&
|
||||
props.shortcutPosition === "before-trailing-icon" &&
|
||||
renderShortcutKey()}
|
||||
|
||||
{TrailingIcon &&
|
||||
(typeof TrailingIcon === "string" ? (
|
||||
<NamedIcon
|
||||
name={TrailingIcon}
|
||||
className={cn(iconClassName, trailingIconClassName, "shrink-0 justify-end")}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
trailingIconClassName,
|
||||
"shrink-0 justify-end",
|
||||
variation.icon
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TrailingIcon
|
||||
className={cn(
|
||||
iconClassName,
|
||||
textColorClassName,
|
||||
variation.icon,
|
||||
trailingIconClassName,
|
||||
"shrink-0 justify-end"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{shortcut && (
|
||||
<ShortcutKey
|
||||
className={cn(shortcutClassName)}
|
||||
shortcut={shortcut}
|
||||
variant={variation.shortcutVariant ?? "medium"}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shortcut &&
|
||||
!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<
|
||||
@@ -268,7 +327,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
className={cn("group/button outline-none focus-custom", props.fullWidth ? "w-full" : "")}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
@@ -314,7 +373,7 @@ export const LinkButton = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group pointer-events-none cursor-default opacity-40 outline-none",
|
||||
"group/button pointer-events-none cursor-default opacity-40 outline-none",
|
||||
props.fullWidth ? "w-full" : ""
|
||||
)}
|
||||
>
|
||||
@@ -328,7 +387,7 @@ export const LinkButton = ({
|
||||
<ExtLink
|
||||
href={to.toString()}
|
||||
ref={innerRef}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
className={cn("group/button focus-custom", props.fullWidth ? "w-full" : "")}
|
||||
onClick={onClick}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseEnter={onMouseEnter}
|
||||
@@ -343,7 +402,7 @@ export const LinkButton = ({
|
||||
<Link
|
||||
to={to}
|
||||
ref={innerRef}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
className={cn("group/button focus-custom", props.fullWidth ? "w-full" : "")}
|
||||
onClick={onClick}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseEnter={onMouseEnter}
|
||||
@@ -364,7 +423,7 @@ export const NavLinkButton = ({ to, className, target, ...props }: NavLinkPropsT
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn("group outline-none", props.fullWidth ? "w-full" : "")}
|
||||
className={cn("group/button outline-none", props.fullWidth ? "w-full" : "")}
|
||||
target={target}
|
||||
>
|
||||
{({ isActive, isPending }) => (
|
||||
|
||||
@@ -61,6 +61,7 @@ export type CheckboxProps = Omit<
|
||||
description?: string;
|
||||
badges?: string[];
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
onChange?: (isChecked: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -78,6 +79,7 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
|
||||
badges,
|
||||
disabled,
|
||||
className,
|
||||
labelClassName: externalLabelClassName,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -148,7 +150,8 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
|
||||
labelClassName
|
||||
labelClassName,
|
||||
externalLabelClassName
|
||||
)}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BellAlertIcon } from "@heroicons/react/20/solid";
|
||||
import { BellAlertIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { CalendarDateTime, createCalendar } from "@internationalized/date";
|
||||
import { useDateField, useDateSegment } from "@react-aria/datepicker";
|
||||
import type { DateFieldState, DateSegment } from "@react-stately/datepicker";
|
||||
@@ -12,7 +12,7 @@ const variants = {
|
||||
small: {
|
||||
fieldStyles: "h-5 text-sm rounded-sm px-0.5",
|
||||
nowButtonVariant: "tertiary/small" as const,
|
||||
clearButtonVariant: "minimal/small" as const,
|
||||
clearButtonVariant: "tertiary/small" as const,
|
||||
},
|
||||
medium: {
|
||||
fieldStyles: "h-7 text-base rounded px-1",
|
||||
@@ -35,9 +35,12 @@ type DateFieldProps = {
|
||||
showNowButton?: boolean;
|
||||
showClearButton?: boolean;
|
||||
onValueChange?: (value: Date | undefined) => void;
|
||||
utc?: boolean;
|
||||
variant?: Variant;
|
||||
};
|
||||
|
||||
const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
export function DateField({
|
||||
label,
|
||||
defaultValue,
|
||||
@@ -50,10 +53,11 @@ export function DateField({
|
||||
showGuide = false,
|
||||
showNowButton = false,
|
||||
showClearButton = false,
|
||||
utc = false,
|
||||
variant = "small",
|
||||
}: DateFieldProps) {
|
||||
const [value, setValue] = useState<undefined | CalendarDateTime>(
|
||||
utcDateToCalendarDate(defaultValue)
|
||||
utc ? utcDateToCalendarDate(defaultValue) : dateToCalendarDate(defaultValue)
|
||||
);
|
||||
|
||||
const state = useDateFieldState({
|
||||
@@ -61,11 +65,11 @@ export function DateField({
|
||||
onChange: (value) => {
|
||||
if (value) {
|
||||
setValue(value);
|
||||
onValueChange?.(value.toDate("utc"));
|
||||
onValueChange?.(value.toDate(utc ? "utc" : deviceTimezone));
|
||||
}
|
||||
},
|
||||
minValue: utcDateToCalendarDate(minValue),
|
||||
maxValue: utcDateToCalendarDate(maxValue),
|
||||
minValue: utc ? utcDateToCalendarDate(minValue) : dateToCalendarDate(minValue),
|
||||
maxValue: utc ? utcDateToCalendarDate(maxValue) : dateToCalendarDate(maxValue),
|
||||
shouldForceLeadingZeros: true,
|
||||
granularity,
|
||||
locale: "en-US",
|
||||
@@ -78,7 +82,9 @@ export function DateField({
|
||||
useEffect(() => {
|
||||
if (state.value === undefined && defaultValue === undefined) return;
|
||||
|
||||
const calendarDate = utcDateToCalendarDate(defaultValue);
|
||||
const calendarDate = utc
|
||||
? utcDateToCalendarDate(defaultValue)
|
||||
: dateToCalendarDate(defaultValue);
|
||||
//unchanged
|
||||
if (state.value?.toDate("utc").getTime() === defaultValue?.getTime()) {
|
||||
return;
|
||||
@@ -134,23 +140,19 @@ export function DateField({
|
||||
<Button
|
||||
type="button"
|
||||
variant={variants[variant].nowButtonVariant}
|
||||
LeadingIcon={BellAlertIcon}
|
||||
leadingIconClassName="text-text-dimmed group-hover:text-text-bright"
|
||||
onClick={() => {
|
||||
const now = new Date();
|
||||
setValue(utcDateToCalendarDate(new Date()));
|
||||
setValue(utc ? utcDateToCalendarDate(now) : dateToCalendarDate(now));
|
||||
onValueChange?.(now);
|
||||
}}
|
||||
>
|
||||
<span className="text-text-dimmed transition group-hover:text-text-bright">Now</span>
|
||||
Now
|
||||
</Button>
|
||||
)}
|
||||
{showClearButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant={variants[variant].clearButtonVariant}
|
||||
LeadingIcon={"close"}
|
||||
leadingIconClassName="-mr-2"
|
||||
onClick={() => {
|
||||
setValue(undefined);
|
||||
onValueChange?.(undefined);
|
||||
@@ -181,7 +183,7 @@ function utcDateToCalendarDate(date?: Date) {
|
||||
return date
|
||||
? new CalendarDateTime(
|
||||
date.getUTCFullYear(),
|
||||
date.getUTCMonth(),
|
||||
date.getUTCMonth() + 1,
|
||||
date.getUTCDate(),
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
@@ -190,6 +192,19 @@ function utcDateToCalendarDate(date?: Date) {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function dateToCalendarDate(date?: Date) {
|
||||
return date
|
||||
? new CalendarDateTime(
|
||||
date.getFullYear(),
|
||||
date.getMonth() + 1,
|
||||
date.getDate(),
|
||||
date.getHours(),
|
||||
date.getMinutes(),
|
||||
date.getSeconds()
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
type DateSegmentProps = {
|
||||
segment: DateSegment;
|
||||
state: DateFieldState;
|
||||
|
||||
@@ -34,6 +34,7 @@ type DetailCellProps = {
|
||||
description?: string | React.ReactNode;
|
||||
className?: string;
|
||||
variant?: keyof typeof variations;
|
||||
boxClassName?: string;
|
||||
};
|
||||
|
||||
export function DetailCell({
|
||||
@@ -45,6 +46,7 @@ export function DetailCell({
|
||||
description,
|
||||
className,
|
||||
variant = "small",
|
||||
boxClassName,
|
||||
}: DetailCellProps) {
|
||||
const variation = variations[variant];
|
||||
|
||||
@@ -53,6 +55,7 @@ export function DetailCell({
|
||||
<IconInBox
|
||||
icon={leadingIcon}
|
||||
className={cn("flex-none transition group-hover:border-charcoal-750", leadingIconClassName)}
|
||||
boxClassName={boxClassName}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<Paragraph
|
||||
|
||||
@@ -50,7 +50,7 @@ const DialogContent = React.forwardRef<
|
||||
>
|
||||
<hr className="absolute left-0 top-11 w-full" />
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background data-[state=open]:bg-accent data-[state=open]:text-muted-foreground focus-visible:ring-ring absolute right-3 top-3 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none">
|
||||
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-3 top-2 rounded-sm py-1 pr-1 opacity-70 transition-opacity focus-custom hover:opacity-100 disabled:pointer-events-none">
|
||||
<div className="flex gap-x-2">
|
||||
<ShortcutKey
|
||||
shortcut={{
|
||||
@@ -82,7 +82,7 @@ DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
className={cn("flex justify-between border-t border-grid-bright pt-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -106,7 +106,7 @@ const DialogDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-muted-foreground pt-4 text-sm", className)}
|
||||
className={cn("pt-2 text-base text-text-dimmed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -10,7 +10,12 @@ export function FormButtons({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex w-full items-center justify-between", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between border-t border-grid-bright pt-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{cancelButton ? cancelButton : <div />} {confirmButton}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { LinkButton } from "./Buttons";
|
||||
import { Header3 } from "./Headers";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { LinkButton } from "./Buttons";
|
||||
import { Header2 } from "./Headers";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const variants = {
|
||||
info: {
|
||||
panelStyle: "border-grid-bright bg-background-bright",
|
||||
panelStyle: "border-grid-bright bg-background-bright rounded-md border p-4 gap-3",
|
||||
},
|
||||
upgrade: {
|
||||
panelStyle: "border-indigo-400/20 bg-indigo-800/10",
|
||||
panelStyle: "border-indigo-400/20 bg-indigo-800/10 rounded-md border p-4 gap-3",
|
||||
},
|
||||
minimal: {
|
||||
panelStyle: "max-w-full w-full py-3 px-3 gap-2",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +46,7 @@ export function InfoPanel({
|
||||
className={cn(
|
||||
variantStyle.panelStyle,
|
||||
title ? "flex-col" : "",
|
||||
"flex h-fit items-start gap-3 rounded-md border p-4",
|
||||
"flex h-fit items-start",
|
||||
panelClassName
|
||||
)}
|
||||
>
|
||||
@@ -57,7 +60,7 @@ export function InfoPanel({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{title && <Header3 className="text-text-bright">{title}</Header3>}
|
||||
{title && <Header2 className="text-text-bright">{title}</Header2>}
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className="text-text-dimmed">
|
||||
{children}
|
||||
|
||||
@@ -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-ring 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-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";
|
||||
|
||||
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";
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
|
||||
<div className="group -ml-1.5 flex items-center gap-0">
|
||||
<Link
|
||||
to={backButton.to}
|
||||
className="rounded px-1.5 py-1 text-xs text-text-dimmed transition group-hover:bg-charcoal-700 group-hover:text-text-bright"
|
||||
className="rounded px-1.5 py-1 text-xs text-text-dimmed transition focus-custom group-hover:bg-charcoal-700 group-hover:text-text-bright"
|
||||
>
|
||||
{backButton.text}
|
||||
</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,
|
||||
@@ -20,17 +19,18 @@ export function PaginationControls({
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="flex items-center gap-1" aria-label="Pagination">
|
||||
<LinkDisabled
|
||||
<nav className="flex items-center gap-0.5" aria-label="Pagination">
|
||||
<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/medium" LeadingIcon={ChevronLeftIcon}>
|
||||
Previous
|
||||
</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/medium" TrailingIcon={ChevronRightIcon}>
|
||||
Next
|
||||
</ButtonContent>
|
||||
</LinkDisabled>
|
||||
Next
|
||||
</LinkButton>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -61,9 +62,9 @@ function pageUrl(location: ReturnType<typeof useLocation>, page: number): string
|
||||
}
|
||||
|
||||
const baseClass =
|
||||
"flex items-center justify-center border border-transparent h-8 w-8 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-text-dimmed text-text-bright hover:bg-tertiary";
|
||||
const selectedClass = "border-charcoal-600 bg-tertiary text-text-bright hover:bg-charcoal-600/50";
|
||||
|
||||
function PageLinkComponent({
|
||||
page,
|
||||
|
||||
@@ -6,6 +6,8 @@ import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { type ButtonContentPropsType, LinkButton } from "./Buttons";
|
||||
import { Paragraph, type ParagraphVariant } from "./Paragraph";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
@@ -91,7 +93,7 @@ function PopoverCustomTrigger({
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
"group flex items-center justify-end gap-1 rounded text-text-dimmed transition hover:bg-charcoal-850 hover:text-text-bright",
|
||||
"group flex items-center justify-end gap-1 rounded text-text-dimmed transition focus-custom hover:bg-charcoal-850 hover:text-text-bright",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -100,6 +102,45 @@ function PopoverCustomTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverSideMenuTrigger({
|
||||
isOpen,
|
||||
children,
|
||||
className,
|
||||
shortcut,
|
||||
...props
|
||||
}: { isOpen?: boolean; shortcut?: ShortcutDefinition } & React.ComponentPropsWithoutRef<
|
||||
typeof PopoverTrigger
|
||||
>) {
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
useShortcutKeys({
|
||||
shortcut: shortcut,
|
||||
action: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (ref.current) {
|
||||
ref.current.click();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-[1.8rem] shrink-0 select-none items-center gap-x-1.5 rounded-sm bg-transparent px-[0.4rem] text-center font-sans text-2sm font-normal text-text-bright transition duration-150 focus-custom hover:bg-charcoal-750",
|
||||
shortcut ? "justify-between" : "",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{shortcut && (
|
||||
<ShortcutKey className={cn("size-4 flex-none")} shortcut={shortcut} variant={"small"} />
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverArrowTrigger({
|
||||
isOpen,
|
||||
children,
|
||||
@@ -116,7 +157,7 @@ function PopoverArrowTrigger({
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
"group flex h-6 items-center gap-1 rounded px-2 text-text-dimmed transition hover:bg-charcoal-700 hover:text-text-bright",
|
||||
"group flex h-6 items-center gap-1 rounded px-2 text-text-dimmed transition focus-custom hover:bg-charcoal-700 hover:text-text-bright",
|
||||
fullWidth && "w-full justify-between",
|
||||
className
|
||||
)}
|
||||
@@ -149,11 +190,11 @@ function PopoverVerticalEllipseTrigger({
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
"group flex items-center justify-end gap-1 rounded px-1.5 py-1.5 text-text-dimmed transition hover:bg-charcoal-750 hover:text-text-bright",
|
||||
"group flex items-center justify-end gap-1 rounded-[3px] p-0.5 text-text-dimmed transition focus-custom hover:bg-tertiary hover:text-text-bright",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<EllipsisVerticalIcon className={cn("h-5 w-5 transition group-hover:text-text-bright")} />
|
||||
<EllipsisVerticalIcon className={cn("size-5 transition group-hover:text-text-bright")} />
|
||||
</PopoverTrigger>
|
||||
);
|
||||
}
|
||||
@@ -165,6 +206,7 @@ export {
|
||||
PopoverCustomTrigger,
|
||||
PopoverMenuItem,
|
||||
PopoverSectionHeader,
|
||||
PopoverSideMenuTrigger,
|
||||
PopoverTrigger,
|
||||
PopoverVerticalEllipseTrigger,
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ export function RadioButtonCircle({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:ring-ring aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-full border border-charcoal-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"ring-offset-background aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-full border border-charcoal-600 focus-custom disabled:cursor-not-allowed disabled:opacity-50",
|
||||
boxClassName
|
||||
)}
|
||||
>
|
||||
@@ -81,7 +81,9 @@ export function RadioButtonCircle({
|
||||
outerCircleClassName
|
||||
)}
|
||||
>
|
||||
<Circle className={cn("h-1.5 w-1.5 fill-white text-white", innerCircleClassName)} />
|
||||
<Circle
|
||||
className={cn("size-1.5 fill-text-bright text-text-bright", innerCircleClassName)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -121,7 +123,7 @@ export const RadioGroupItem = React.forwardRef<
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex cursor-pointer items-start gap-x-2 transition",
|
||||
"group flex cursor-pointer items-start gap-x-2 transition focus-custom",
|
||||
variation.button,
|
||||
className
|
||||
)}
|
||||
@@ -129,7 +131,7 @@ export const RadioGroupItem = React.forwardRef<
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:ring-ring aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-full border border-charcoal-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"ring-offset-background focus-visible:ring-ring aspect-square h-4 w-4 shrink-0 overflow-hidden rounded-full border border-charcoal-600 focus-custom disabled:cursor-not-allowed disabled:opacity-50",
|
||||
variation.inputPosition
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,7 @@ const ResizableHandle = ({
|
||||
}) => (
|
||||
<PanelResizer
|
||||
className={cn(
|
||||
"focus-visible:ring-ring group relative flex w-0.75 items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 hover:w-0.75 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary/50 focus-visible:ring-offset-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
"group relative flex w-0.75 items-center justify-center focus-custom after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 hover:w-0.75 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
size="3px"
|
||||
|
||||
@@ -65,10 +65,7 @@ export default function SegmentedControl({
|
||||
value={option.value}
|
||||
className={({ active, checked }) =>
|
||||
cn(
|
||||
"relative flex h-full grow cursor-pointer text-center font-normal focus:outline-none",
|
||||
active
|
||||
? "ring-offset-2 focus-visible:ring focus-visible:ring-secondary focus-visible:ring-opacity-60"
|
||||
: "",
|
||||
"relative flex h-full grow cursor-pointer text-center font-normal focus-custom",
|
||||
checked
|
||||
? variants[variant].active
|
||||
: "text-text-dimmed transition hover:text-text-bright"
|
||||
|
||||
@@ -15,18 +15,18 @@ const sizes = {
|
||||
button: "h-6 rounded text-xs px-2 ",
|
||||
},
|
||||
medium: {
|
||||
button: "h-8 rounded text-xs px-3 text-sm",
|
||||
button: "h-8 rounded px-3 text-sm",
|
||||
},
|
||||
};
|
||||
|
||||
const style = {
|
||||
tertiary: {
|
||||
button:
|
||||
"bg-tertiary focus-within:ring-charcoal-500 border border-tertiary hover:text-text-bright hover:border-charcoal-600",
|
||||
"bg-tertiary focus-custom border border-tertiary hover:text-text-bright hover:border-charcoal-600",
|
||||
},
|
||||
minimal: {
|
||||
button:
|
||||
"bg-transparent focus-within:ring-charcoal-500 hover:bg-tertiary disabled:bg-transparent disabled:pointer-events-none",
|
||||
"bg-transparent focus-custom hover:bg-tertiary disabled:bg-transparent disabled:pointer-events-none",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -322,7 +322,7 @@ export function SelectTrigger({
|
||||
render={
|
||||
<Ariakit.Select
|
||||
className={cn(
|
||||
"group flex items-center gap-1 outline-offset-0 focus-within:outline-none focus-within:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"group flex items-center gap-1 focus-custom disabled:cursor-not-allowed disabled:opacity-50",
|
||||
variantClasses.button,
|
||||
className
|
||||
)}
|
||||
@@ -426,7 +426,7 @@ export function SelectList(props: SelectListProps) {
|
||||
<Component
|
||||
{...props}
|
||||
className={cn(
|
||||
"overflow-y-auto overscroll-contain outline-none scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
"overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 focus-custom",
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
@@ -440,11 +440,11 @@ export interface SelectItemProps extends Ariakit.SelectItemProps {
|
||||
}
|
||||
|
||||
const selectItemClasses =
|
||||
"group cursor-pointer px-1 pt-1 text-xs text-text-dimmed outline-none last:pb-1";
|
||||
"group cursor-pointer px-1 pt-1 text-2sm text-text-dimmed focus-custom last:pb-1";
|
||||
|
||||
export function SelectItem({
|
||||
icon,
|
||||
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-white" />,
|
||||
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-text-bright" />,
|
||||
shortcut,
|
||||
...props
|
||||
}: SelectItemProps) {
|
||||
@@ -477,12 +477,16 @@ export function SelectItem({
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="flex h-7 w-full items-center gap-1 rounded-sm px-2 group-data-[active-item=true]:bg-tertiary">
|
||||
<div className="flex h-8 w-full items-center gap-1 rounded-sm px-2 group-data-[active-item=true]:bg-tertiary">
|
||||
{icon}
|
||||
<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>
|
||||
@@ -613,7 +617,7 @@ export function SelectPopover({
|
||||
"z-50 flex flex-col overflow-clip rounded border border-charcoal-700 bg-background-bright shadow-md outline-none animate-in fade-in-40",
|
||||
"min-w-[max(180px,calc(var(--popover-anchor-width)+0.5rem))]",
|
||||
"max-w-[min(480px,var(--popover-available-width))]",
|
||||
"max-h-[min(480px,var(--popover-available-height))]",
|
||||
"max-h-[min(600px,var(--popover-available-height))]",
|
||||
"origin-[var(--popover-transform-origin)]",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ export function Spinner({
|
||||
foreground: "#3C4B62",
|
||||
},
|
||||
dark: {
|
||||
background: "#15171A",
|
||||
foreground: "#272A2E",
|
||||
background: "rgba(18, 19, 23, 0.35)",
|
||||
foreground: "#1A1B1F",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ import { cn } from "~/utils/cn";
|
||||
|
||||
const variations = {
|
||||
large: {
|
||||
container: "flex items-center gap-x-2 rounded-md hover:bg-tertiary p-2 transition",
|
||||
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 gap-x-1.5 rounded hover:bg-tertiary pr-1 py-[0.1rem] pl-1.5 transition",
|
||||
"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 text-charcoal-400 group-hover:text-charcoal-200 hover:cursor-pointer transition",
|
||||
thumb: "size-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-xs text-text-dimmed",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -38,14 +38,14 @@ export const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
"group-focus-visible:ring-ring group-focus-visible:ring-offset-background peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors group-focus-visible:ring-2 group-focus-visible:ring-offset-2 group-disabled:cursor-not-allowed group-disabled:opacity-50 group-data-[state=checked]:bg-secondary group-data-[state=unchecked]:bg-charcoal-700 focus-visible:outline-none",
|
||||
"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
|
||||
)}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
thumb,
|
||||
"pointer-events-none block rounded-full bg-charcoal-200 shadow-lg ring-0 transition group-data-[state=checked]:bg-charcoal-700"
|
||||
"pointer-events-none block rounded-full bg-charcoal-200 transition group-data-[state=checked]:bg-text-bright"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,12 +17,12 @@ export const Table = forwardRef<HTMLTableElement, TableProps>(
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-x-auto whitespace-nowrap rounded-md border border-grid-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
"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 divide-y", className)}>
|
||||
<table ref={ref} className={cn("w-full", className)}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
@@ -40,7 +40,10 @@ export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>
|
||||
return (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn("rounded-t-md", "relative divide-y divide-grid-dimmed", className)}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</thead>
|
||||
@@ -56,7 +59,7 @@ type TableBodyProps = {
|
||||
export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
|
||||
({ className, children }, ref) => {
|
||||
return (
|
||||
<tbody ref={ref} className={cn("relative divide-y divide-grid-dimmed", className)}>
|
||||
<tbody ref={ref} className={cn("relative overflow-y-auto", className)}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
@@ -67,12 +70,21 @@ 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) => {
|
||||
return (
|
||||
<tr ref={ref} className={cn(disabled && "opacity-50", "group/table-row w-full", className)}>
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group/table-row relative w-full after:absolute after:bottom-0 after:left-3 after:right-0 after:h-px after:bg-grid-dimmed",
|
||||
disabled && "opacity-50",
|
||||
isSelected && isSelectedStyle,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
@@ -108,7 +120,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
|
||||
ref={ref}
|
||||
scope="col"
|
||||
className={cn(
|
||||
"px-3 py-2 align-middle text-xxs font-normal uppercase tracking-wider text-text-dimmed",
|
||||
"px-3 py-2.5 pb-3 align-middle text-sm font-medium text-text-bright",
|
||||
alignmentClassName,
|
||||
className
|
||||
)}
|
||||
@@ -135,10 +147,23 @@ type TableCellProps = TableCellBasicProps & {
|
||||
hasAction?: boolean;
|
||||
isSticky?: boolean;
|
||||
actionClassName?: string;
|
||||
rowHoverStyle?: keyof typeof rowHoverStyles;
|
||||
isSelected?: boolean;
|
||||
};
|
||||
|
||||
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 z-10 w-[2.8rem] min-w-[2.8rem] bg-background-dimmed before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover/table-row:before:to-charcoal-900";
|
||||
"sticky right-0 bg-background-dimmed group-hover/table-row:bg-charcoal-750 w-[--sticky-width] [&:has(.group-hover\\/table-row\\:block)]:w-auto";
|
||||
|
||||
const isSelectedStyle = "bg-charcoal-750 group-hover:bg-charcoal-750";
|
||||
|
||||
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
(
|
||||
@@ -152,6 +177,8 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
onClick,
|
||||
hasAction = false,
|
||||
isSticky = false,
|
||||
rowHoverStyle = "default",
|
||||
isSelected,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -179,21 +206,21 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-xs text-charcoal-400",
|
||||
to || onClick || hasAction
|
||||
? "cursor-pointer group-hover/table-row:bg-charcoal-900"
|
||||
: "px-3 py-3 align-middle",
|
||||
to || onClick || hasAction ? "cursor-pointer" : "px-3 py-3 align-middle",
|
||||
!to && !onClick && alignmentClassName,
|
||||
isSticky && stickyStyles,
|
||||
isSelected && isSelectedStyle,
|
||||
!isSelected && rowHoverStyles[rowHoverStyle],
|
||||
className
|
||||
)}
|
||||
colSpan={colSpan}
|
||||
>
|
||||
{to ? (
|
||||
<Link to={to} className={cn(flexClasses, actionClassName)}>
|
||||
<Link to={to} className={cn("focus-custom", flexClasses, actionClassName)}>
|
||||
{children}
|
||||
</Link>
|
||||
) : onClick ? (
|
||||
<button onClick={onClick} className={cn(flexClasses, actionClassName)}>
|
||||
<button onClick={onClick} className={cn("focus-custom", flexClasses, actionClassName)}>
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
@@ -224,7 +251,7 @@ export const TableCellChevron = forwardRef<
|
||||
alignment="right"
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="h-4 w-4 text-text-dimmed transition group-hover:text-text-bright" />
|
||||
<ChevronRightIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
|
||||
</TableCell>
|
||||
);
|
||||
});
|
||||
@@ -233,33 +260,90 @@ export const TableCellMenu = forwardRef<
|
||||
HTMLTableCellElement,
|
||||
{
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
isSticky?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
visibleButtons?: ReactNode;
|
||||
hiddenButtons?: ReactNode;
|
||||
popoverContent?: ReactNode;
|
||||
children?: ReactNode;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
>(({ className, children, isSticky, onClick }, ref) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<TableCell
|
||||
className={className}
|
||||
isSticky={isSticky}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
hasAction={true}
|
||||
>
|
||||
<Popover onOpenChange={(open) => setIsOpen(open)}>
|
||||
<PopoverVerticalEllipseTrigger isOpen={isOpen} />
|
||||
<PopoverContent
|
||||
className="w-fit max-w-[10rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex flex-col gap-1 p-1">{children}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TableCell>
|
||||
);
|
||||
});
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
isSticky,
|
||||
onClick,
|
||||
visibleButtons,
|
||||
hiddenButtons,
|
||||
popoverContent,
|
||||
children,
|
||||
isSelected,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
className={className}
|
||||
isSticky={isSticky}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
hasAction={true}
|
||||
isSelected={isSelected}
|
||||
>
|
||||
<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] bg-background-dimmed p-0.5 group-hover/table-row:bg-background-bright group-hover/table-row:ring-1 group-hover/table-row:ring-grid-bright",
|
||||
isSelected && isSelectedStyle,
|
||||
isSelected &&
|
||||
"group-hover/table-row:bg-charcoal-750 group-hover/table-row:ring-charcoal-600/50"
|
||||
)}
|
||||
>
|
||||
{/* 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">
|
||||
{hiddenButtons}
|
||||
</div>
|
||||
)}
|
||||
{/* Always visible buttons */}
|
||||
{visibleButtons}
|
||||
{/* Always visible popover with ellipsis trigger */}
|
||||
{popoverContent && (
|
||||
<Popover onOpenChange={(open) => setIsOpen(open)}>
|
||||
<PopoverVerticalEllipseTrigger
|
||||
isOpen={isOpen}
|
||||
className="duration-0 group-hover/table-row:text-text-bright"
|
||||
/>
|
||||
<PopoverContent
|
||||
className="min-w-[10rem] max-w-[20rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex flex-col gap-1 p-1">{popoverContent}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{/* Optionally pass in children to render in a popover */}
|
||||
{!visibleButtons && !hiddenButtons && !popoverContent && (
|
||||
<Popover onOpenChange={(open) => setIsOpen(open)}>
|
||||
<PopoverVerticalEllipseTrigger isOpen={isOpen} />
|
||||
<PopoverContent
|
||||
className="w-fit max-w-[10rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex flex-col gap-1 p-1">{children}</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TableBlankRowProps = {
|
||||
className?: string;
|
||||
|
||||
@@ -46,7 +46,7 @@ export function TabLink({
|
||||
layoutId: string;
|
||||
}) {
|
||||
return (
|
||||
<NavLink to={to} className="group flex flex-col items-center pt-1" end>
|
||||
<NavLink to={to} className="group flex flex-col items-center pt-1 focus-custom" end>
|
||||
{({ isActive, isPending }) => {
|
||||
return (
|
||||
<>
|
||||
@@ -96,7 +96,8 @@ export function TabButton({
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("group flex flex-col items-center pt-1", props.className)}
|
||||
className={cn("group flex flex-col items-center pt-1 focus-custom", props.className)}
|
||||
type="button"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ export function TextArea({ className, rows, ...props }: TextAreaProps) {
|
||||
{...props}
|
||||
rows={rows ?? 6}
|
||||
className={cn(
|
||||
"ring-offset-background placeholder:text-muted-foreground focus:border-ring focus:ring-ring focus-visible:ring-ring w-full rounded-md border border-tertiary bg-tertiary px-3 text-sm text-text-bright transition file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 focus:outline-none focus:ring-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"placeholder:text-muted-foreground w-full rounded border border-charcoal-800 bg-charcoal-750 px-3 text-sm text-text-bright transition focus-custom focus-custom file:border-0 file:bg-transparent file:text-base file:font-medium hover:border-charcoal-600 hover:bg-charcoal-650 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -41,7 +41,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
"z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-none",
|
||||
variantClasses[variant],
|
||||
className
|
||||
)}
|
||||
@@ -72,7 +72,9 @@ function SimpleTooltip({
|
||||
return (
|
||||
<TooltipProvider disableHoverableContent={disableHoverableContent}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className={cn("h-fit", buttonClassName)}>{button}</TooltipTrigger>
|
||||
<TooltipTrigger tabIndex={-1} className={cn("h-fit", buttonClassName)}>
|
||||
{button}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={side}
|
||||
hidden={hidden}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user