Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6881de84ab | |||
| 0ca8296b9a | |||
| 55946fcdb7 | |||
| c0f9f10b07 | |||
| e80aa2826d | |||
| 1be25d7639 | |||
| 3c3a5e1f28 | |||
| 8e741c7218 | |||
| 83455e3ad4 | |||
| f0f0420280 | |||
| c4533c36cb | |||
| f8e034c190 | |||
| 21f62e780c | |||
| c3564ca0d4 | |||
| 6e1b8a11d4 | |||
| 9900f19b70 | |||
| 237545754f | |||
| d7c1e98e5e | |||
| 71a0afeb8d | |||
| 36a390449a | |||
| 375e495423 | |||
| 866c20cd39 | |||
| 408785b8ef | |||
| f982e00997 | |||
| 7e100b8362 | |||
| ea052c44c7 | |||
| 76224568f4 | |||
| 72e67981b0 | |||
| cc44aff24b | |||
| 07d28ee4f5 | |||
| f16a2cc4e6 | |||
| b859837564 | |||
| 1343003efa | |||
| 0c4aea0338 | |||
| 627c767c64 | |||
| f3a0c3a5d1 | |||
| 7bca3d9c15 | |||
| 01fc1c6634 | |||
| 2d4f85e9f4 | |||
| 8718b28949 | |||
| 2caa2a5bbd | |||
| 43a10ee695 | |||
| 018005d478 | |||
| 4da187c4c5 | |||
| 4925424778 | |||
| 7ef61222ed | |||
| 044d38e390 | |||
| e7768881af | |||
| 5ea6a49d08 | |||
| dce5578327 | |||
| fde3f17a25 | |||
| a6c9cac26c | |||
| 960d99461a | |||
| e5307133b3 | |||
| 34bb3acc23 | |||
| 6d3b761cd7 | |||
| abc9737a4f | |||
| 84af64165c | |||
| 0adf41c7f0 | |||
| b7f4e40edb | |||
| cf33396dc0 | |||
| 51f2cd6b3f | |||
| 513d0f071d | |||
| 50e3192453 | |||
| 1524c95a7a | |||
| c8aaea8ad0 | |||
| cf7f25abcd | |||
| 99453e8581 | |||
| bc7206d981 | |||
| 64b8e150ea | |||
| b3b5852a88 | |||
| d1ecd6b99c | |||
| 0fb16a7324 | |||
| 4f317055fd | |||
| 3afd5f8cf3 | |||
| c922d24941 | |||
| 39ec34f84f | |||
| b9eba68078 | |||
| 9df93d0798 | |||
| 6769d6b439 | |||
| 70410d33af | |||
| 6371dd4f95 | |||
| ebe78017ef | |||
| b697a19954 | |||
| 29ca09e42d | |||
| 375b788ab0 | |||
| d6b44d13f7 | |||
| c6784ba7b8 | |||
| 6b8b1c7cae | |||
| 0c20756624 | |||
| 3d387d602d | |||
| 5d45dea0e1 | |||
| a25751e805 | |||
| 659be8cbc1 | |||
| 50e3d9e43a | |||
| 203a431350 | |||
| 39a332eff1 | |||
| 498f56de3e | |||
| b23696b255 | |||
| f3a7e2aa20 | |||
| b737f7af92 | |||
| f54a9ff5fc | |||
| 067c0db543 | |||
| 773765f82d | |||
| 624072c23b | |||
| d37576f814 | |||
| 476e2f035f | |||
| ad71964d30 | |||
| 975c5f1d3c | |||
| 916ed4e366 | |||
| df4667b7f5 | |||
| d663ba8985 | |||
| 8c8d1598e3 | |||
| 646bfd37bd | |||
| c0d5292b06 | |||
| 8c383bdc69 | |||
| 97e63d1222 | |||
| 87ee98aff1 | |||
| c070e78d06 | |||
| 1c4f9ff1ff | |||
| ca031a72e7 | |||
| cbc976188e | |||
| 59a94c710e | |||
| 4cd97c81ea | |||
| d0d5a698ec | |||
| ee033a1fd4 | |||
| 2394fcdf35 | |||
| 6edaffeb7e | |||
| 0558b2c595 | |||
| b12bc6042f | |||
| a7bc5df754 | |||
| 1c524c5acc | |||
| 65ad98c20d | |||
| 24d892e322 | |||
| be9b51131d | |||
| 63a65dabe4 | |||
| 1f3b423c22 | |||
| a69a5019f1 |
@@ -0,0 +1,65 @@
|
||||
name: "🧪 E2E Tests"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
e2e:
|
||||
name: "🧪 E2E Tests"
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
# Setup environment variables
|
||||
cp ./.env.example ./.env
|
||||
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Execute tests
|
||||
pnpm run docker
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run test:e2e
|
||||
|
||||
# Cleanup
|
||||
pnpm run docker:stop
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,31 @@
|
||||
name: 🤖 PR Checks
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
uses: ./.github/workflows/typecheck.yml
|
||||
secrets: inherit
|
||||
|
||||
units:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
@@ -38,125 +38,19 @@ env:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: ʦ TypeScript
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: ./.github/workflows/typecheck.yml
|
||||
secrets: inherit
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck --filter webapp
|
||||
|
||||
unitTests:
|
||||
name: Unit Tests
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
pnpm run test
|
||||
units:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: e2e Tests
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
# Setup environment variables
|
||||
cp ./.env.example ./.env
|
||||
cp ./references/nextjs-test/.env.example ./references/nextjs-test/.env.local
|
||||
|
||||
# Build packages
|
||||
pnpm run build --filter @references/nextjs-test^...
|
||||
pnpm --filter @trigger.dev/database generate
|
||||
|
||||
# Move trigger-cli bin to correct place
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Execute tests
|
||||
pnpm run docker
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run test:e2e
|
||||
|
||||
# Cleanup
|
||||
pnpm run docker:stop
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
needs: [typecheck, unitTests, e2e]
|
||||
needs: [typecheck, units, e2e]
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -4,16 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/release.yml"
|
||||
- "packages/**"
|
||||
- "!packages/**/*.md"
|
||||
- ".changeset/**"
|
||||
- "integrations/**"
|
||||
- "!integrations/**/*.md"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "turbo.json"
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: "ʦ TypeScript"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 📀 Generate Prisma Client
|
||||
run: pnpm run generate
|
||||
|
||||
- name: 🔎 Type check
|
||||
run: pnpm run typecheck --filter webapp
|
||||
@@ -0,0 +1,30 @@
|
||||
name: "🧪 Unit Tests"
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
unitTests:
|
||||
name: "🧪 Unit Tests"
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.18
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
pnpm run test
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"astro-build.astro-vscode",
|
||||
"denoland.vscode-deno"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deno.enablePaths": ["references/deno-reference"]
|
||||
}
|
||||
+23
-4
@@ -32,7 +32,26 @@ Please follow the best-practice of adding changesets in the same commit as the c
|
||||
|
||||
!MAKE SURE TO UPDATE THE TAG IN THE INSTRUCTIONS BELOW!
|
||||
|
||||
1. Add changesets as usual `pnpm run changeset:add`
|
||||
2. Create a snapshot version (replace "dev" with your tag) `pnpm exec changeset version --snapshot dev`
|
||||
3. Build the packages: `pnpm run build --filter "@trigger.dev/*"`
|
||||
4. Publish the snapshot (replace "dev" with your tag) `pnpm exec changeset publish --no-git-tag --snapshot --tag dev`
|
||||
1. Add changesets as usual
|
||||
|
||||
```sh
|
||||
pnpm run changeset:add
|
||||
```
|
||||
|
||||
2. Create a snapshot version (replace "prerelease" with your tag)
|
||||
|
||||
```sh
|
||||
pnpm exec changeset version --snapshot prerelease
|
||||
```
|
||||
|
||||
3. Build the packages:
|
||||
|
||||
```sh
|
||||
pnpm run build --filter "@trigger.dev/*"
|
||||
```
|
||||
|
||||
4. Publish the snapshot (replace "dev" with your tag)
|
||||
|
||||
```sh
|
||||
pnpm exec changeset publish --no-git-tag --snapshot --tag prerelease
|
||||
```
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
- Other conduct that could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
|
||||
@@ -52,9 +52,13 @@ branch are tagged into a release monthly.
|
||||
Feel free to update `SESSION_SECRET` and `MAGIC_LINK_SECRET` as well using the same method.
|
||||
|
||||
6. Start Docker. This starts the required services like Postgres. If this is your first time using Docker, consider going through this [guide](DOCKER_INSTALLATION.md)
|
||||
|
||||
```
|
||||
pnpm run docker
|
||||
```
|
||||
|
||||
This will also start and run a local instance of [pgAdmin](https://www.pgadmin.org/) on [localhost:5480](http://localhost:5480), preconfigured with email `admin@example.com` and pwd `admin`. Then use `postgres` as the password to the Trigger.dev server.
|
||||
|
||||
7. Migrate the database
|
||||
```
|
||||
pnpm run db:migrate
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
|
||||
</div>
|
||||
|
||||
# ✨🎃 Get involved with Hacktoberfest 2023! 🎃✨
|
||||
|
||||
All of October we're participating in Hacktoberfest and invite you to join us! We have a bunch of issues labeled `🎃 Hacktoberfest` that are ready for you to work on which will count towards Hacktoberfest. We are also running our own game, earn 💎 points to win swag!
|
||||
|
||||
- Check out our [Hacktoberfest landing page](https://trigger.dev/hacktoberfest) for how to participate and win swag.
|
||||
- Contribute to either our [/trigger.dev](https://github.com/triggerdotdev/trigger.dev/labels/%F0%9F%8E%83%20hacktoberfest), [/api-reference](https://github.com/triggerdotdev/api-reference/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%8E%83hacktoberfest) or [/jobs-showcase](https://github.com/triggerdotdev/jobs-showcase/labels/%F0%9F%8E%83%20hacktoberfest) repositories and complete issues marked `🎃 Hacktoberfest` to be eligible for swag.
|
||||
- Join our [Discord](https://discord.gg/JtBAxBr2m3) and get involved in with the community.
|
||||
|
||||
_New to Hacktober? Check out the [Hacktoberfest website](https://hacktoberfest.digitalocean.com/) for more information._
|
||||
|
||||
🎃 **Happy Hacking!** 🎃
|
||||
|
||||
# About Trigger.dev
|
||||
|
||||
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
|
||||
@@ -62,8 +74,8 @@ Click the links to join the discussions about our upcoming features.
|
||||
| Dashboard | View every Task in every Run | ✅ |
|
||||
| Serverless | Long-running Jobs on your serverless backend | ✅ |
|
||||
| React hooks | Easily update your UI with Job progress | ✅ |
|
||||
| React frameworks | Support for Remix, Astro, RedwoodJS & more | ✅ |
|
||||
| [Background tasks](https://github.com/triggerdotdev/trigger.dev/discussions/400) | Offload long or intense Tasks to our infrastructure | 🛠️ |
|
||||
| [React frameworks](https://github.com/triggerdotdev/trigger.dev/discussions/411) | Support for Remix, Astro, RedwoodJS & more | 🛠️ |
|
||||
| [Long-running servers](https://github.com/triggerdotdev/trigger.dev/discussions/430) | Run Jobs on your long-running backend | 🛠️ |
|
||||
| Polling Triggers | Subscribe to changes without webhooks | 🕝 |
|
||||
| Vercel integration | Easy deploy and preview environment support | 🕝 |
|
||||
@@ -83,3 +95,9 @@ We provide an official trigger.dev docker image you can use to easily self-host
|
||||
## Development
|
||||
|
||||
To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md).
|
||||
|
||||
## Meet the Amazing People Behind This Project 🚀
|
||||
|
||||
<a href="https://github.com/triggerdotdev/trigger.dev/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=triggerdotdev/trigger.dev" />
|
||||
</a>
|
||||
|
||||
@@ -36,7 +36,8 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
|
||||
|
||||
const [form, { path, feedbackType, message }] = useForm({
|
||||
id: "accept-invite",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
|
||||
@@ -44,28 +44,7 @@ export function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey:
|
||||
);
|
||||
}
|
||||
|
||||
export function RunDevCommand() {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField variant="primary/medium" className="mb-4" value={`npm run dev`} />
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField variant="primary/medium" className="mb-4" value={`pnpm run dev`} />
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField variant="primary/medium" className="mb-4" value={`yarn run dev`} />
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDevCommand() {
|
||||
export function RunDevCommand({ extra }: { extra?: string }) {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
@@ -77,34 +56,67 @@ export function TriggerDevCommand() {
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npx @trigger.dev/cli@latest dev`}
|
||||
value={`npm run dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm dlx @trigger.dev/cli@latest dev`}
|
||||
value={`pnpm run dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn dlx @trigger.dev/cli@latest dev`}
|
||||
value={`yarn run dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDevStep() {
|
||||
export function TriggerDevCommand({ extra }: { extra?: string }) {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm dlx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn dlx @trigger.dev/cli@latest dev${extra ? ` ${extra}` : ""}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDevStep({ extra }: { extra?: string }) {
|
||||
return (
|
||||
<>
|
||||
<Paragraph spacing>
|
||||
In a <span className="text-amber-400">separate terminal window or tab</span> run:
|
||||
</Paragraph>
|
||||
<TriggerDevCommand />
|
||||
<TriggerDevCommand extra={extra} />
|
||||
<Paragraph spacing variant="small">
|
||||
If you’re not running on the default you can specify the port by adding{" "}
|
||||
<InlineCode variant="extra-small">--port 3001</InlineCode> to the end.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { EndpointIndexStatus } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
|
||||
export function EndpointIndexStatusIcon({ status }: { status: EndpointIndexStatus }) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return <ClockIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
case "STARTED":
|
||||
return <Spinner className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
case "SUCCESS":
|
||||
return (
|
||||
<CheckCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />
|
||||
);
|
||||
case "FAILURE":
|
||||
return <XCircleIcon className={cn("h-4 w-4", endpointIndexStatusClassNameColor(status))} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function EndpointIndexStatusLabel({ status }: { status: EndpointIndexStatus }) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "STARTED":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "SUCCESS":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
case "FAILURE":
|
||||
return (
|
||||
<span className={endpointIndexStatusClassNameColor(status)}>
|
||||
{endpointIndexStatusTitle(status)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function endpointIndexStatusTitle(status: EndpointIndexStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "Pending";
|
||||
case "STARTED":
|
||||
return "Started";
|
||||
case "SUCCESS":
|
||||
return "Success";
|
||||
case "FAILURE":
|
||||
return "Failure";
|
||||
}
|
||||
}
|
||||
|
||||
export function endpointIndexStatusClassNameColor(status: EndpointIndexStatus): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "text-dimmed";
|
||||
case "STARTED":
|
||||
return "text-blue-500";
|
||||
case "SUCCESS":
|
||||
return "text-green-500";
|
||||
case "FAILURE":
|
||||
return "text-rose-500";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { Header1, Header2 } from "../primitives/Headers";
|
||||
import { Input } from "../primitives/Input";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { useState } from "react";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { generateTwoRandomWords } from "~/utils/randomWords";
|
||||
|
||||
type ModalProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type ModalContentProps = ModalProps & {
|
||||
randomWord: string;
|
||||
closeModal: () => void;
|
||||
};
|
||||
|
||||
export function RegenerateApiKeyModal({ id, title }: ModalProps) {
|
||||
const randomWord = generateTwoRandomWords();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="tertiary/small" leadingIconClassName="text-dimmed" LeadingIcon="key">
|
||||
Regenerate
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<RegenerateApiKeyModalContent
|
||||
id={id}
|
||||
title={title}
|
||||
randomWord={randomWord}
|
||||
closeModal={() => setOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const RegenerateApiKeyModalContent = ({ id, title, randomWord, closeModal }: ModalContentProps) => {
|
||||
const [confirmationText, setConfirmationText] = useState("");
|
||||
const fetcher = useFetcher();
|
||||
const isSubmitting = fetcher.state === "submitting";
|
||||
|
||||
// form submission completed
|
||||
if (fetcher.state === "loading") {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-y-6">
|
||||
<div className="flex justify-center">
|
||||
<Header1>{title} Environment</Header1>
|
||||
</div>
|
||||
<Header2 className="rounded border border-rose-500 bg-rose-500/10 px-3.5 py-2 text-center text-bright">
|
||||
Are you sure you want to regenerate the keys <br />
|
||||
for this environment?
|
||||
</Header2>
|
||||
<Paragraph variant="small" className="px-6 text-center">
|
||||
<>
|
||||
This will temporarily break any live jobs in the
|
||||
<span className="strong text-bright"> {title} Environment</span> until the new API keys
|
||||
are set in the relevant environment variables.
|
||||
</>
|
||||
</Paragraph>
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={`/resources/environments/${id}/regenerate-api-key`}
|
||||
className="mt-2 flex w-full flex-col items-center gap-2"
|
||||
>
|
||||
<Paragraph variant="small" className="font-medium">
|
||||
To confirm, please enter the text:{" "}
|
||||
<span className="font-bold text-bright">{randomWord}</span>
|
||||
</Paragraph>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Confirmation Text"
|
||||
fullWidth={false}
|
||||
value={confirmationText}
|
||||
onChange={(e) => setConfirmationText(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="primary/large"
|
||||
fullWidth
|
||||
className="mt-4"
|
||||
disabled={confirmationText !== randomWord}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Spinner color="white" />
|
||||
) : (
|
||||
<>
|
||||
<NamedIcon
|
||||
name="key"
|
||||
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
|
||||
/>
|
||||
Regenerate API Keys
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -66,7 +66,7 @@ export function FrameworkSelector() {
|
||||
<FrameworkLink to={projectSetupNuxtPath(organization, project)}>
|
||||
<NuxtLogo className="w-32" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)}>
|
||||
<FrameworkLink to={projectSetupSvelteKitPath(organization, project)} supported>
|
||||
<SvelteKitLogo className="w-44" />
|
||||
</FrameworkLink>
|
||||
<FrameworkLink to={projectSetupFastifyPath(organization, project)}>
|
||||
|
||||
@@ -46,7 +46,8 @@ export function ConnectToOAuthForm({
|
||||
|
||||
const [form, { title, slug, scopes, hasCustomClient, customClientId, customClientSecret }] =
|
||||
useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
// TODO: type this
|
||||
lastSubmission: fetcher.data as any,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
|
||||
@@ -45,7 +45,8 @@ export function UpdateOAuthForm({
|
||||
const { isManagedCloud } = useFeatures();
|
||||
|
||||
const [form, { title, scopes, hasCustomClient, customClientId, customClientSecret }] = useForm({
|
||||
lastSubmission: fetcher.data,
|
||||
// TODO: type this
|
||||
lastSubmission: fetcher.data as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, {
|
||||
schema,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { RouteMatch, useMatches } from "@remix-run/react";
|
||||
import { UIMatch, useMatches } from "@remix-run/react";
|
||||
import { Fragment, ReactNode } from "react";
|
||||
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
|
||||
import { Handle } from "~/utils/handle";
|
||||
|
||||
export type BreadcrumbItem = (match: RouteMatch, allMatches: RouteMatch[]) => ReactNode;
|
||||
export type BreadcrumbItem = (match: UIMatch, allMatches: UIMatch[]) => ReactNode;
|
||||
|
||||
export function Breadcrumb() {
|
||||
const matches = useMatches();
|
||||
const matches = useMatches() as UIMatch<unknown, Handle>[];
|
||||
|
||||
return (
|
||||
<div className="hidden items-center md:flex">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link, RouteMatch } from "@remix-run/react";
|
||||
import { Link, UIMatch } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
PopoverSectionHeader,
|
||||
} from "../primitives/Popover";
|
||||
|
||||
export function JobsMenu({ matches }: { matches: RouteMatch[] }) {
|
||||
export function JobsMenu({ matches }: { matches: UIMatch[] }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const organization = useOrganization(matches);
|
||||
const project = useProject(matches);
|
||||
|
||||
@@ -17,7 +17,6 @@ export function NavBar() {
|
||||
<LogoIcon className="h-5 w-5" />
|
||||
</Link>
|
||||
<Breadcrumb />
|
||||
<MobileDropdownMenu />
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
<LinkButton to={docsRoot()} variant="secondary/small" LeadingIcon={BookOpenIcon}>
|
||||
@@ -46,83 +45,3 @@ export function BreadcrumbLink({ title, to }: { title: string; to: string }) {
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileDropdownMenu() {
|
||||
return (
|
||||
<Popover className="block sm:hidden">
|
||||
<Popover.Button
|
||||
className="bg-slate-70 relative z-10 flex h-8 w-8 items-center justify-center rounded border-none bg-opacity-50 focus-visible:border-none focus-visible:outline-none"
|
||||
aria-label="Toggle Navigation"
|
||||
>
|
||||
{({ open }) => <MobileNavIcon open={open} />}
|
||||
</Popover.Button>
|
||||
<Transition.Root>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="duration-150 ease-out"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="duration-150 ease-in"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Overlay className="absolute left-0 top-0 h-full w-full origin-top bg-midnight-900/80" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="duration-150 ease-out"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel
|
||||
as="div"
|
||||
className="absolute inset-x-6 top-0 mt-20 flex origin-top flex-col gap-4 rounded-md bg-slate-800 p-4 text-lg tracking-tight text-slate-900 shadow-xl ring-1 ring-slate-700"
|
||||
>
|
||||
{/* <PrimaryA
|
||||
href={docsRoot()}
|
||||
target="_blank"
|
||||
className="max-w-full"
|
||||
>
|
||||
<ArrowTopRightOnSquareIcon className="-ml-1 h-4 w-4" />
|
||||
Documentation
|
||||
</PrimaryA>
|
||||
<PrimaryButton
|
||||
data-attr="posthog-feedback-button"
|
||||
className="max-w-full"
|
||||
>
|
||||
<ChatBubbleLeftRightIcon className="-ml-1 h-4 w-4" />
|
||||
Send us feedback
|
||||
</PrimaryButton>
|
||||
<SecondaryLink to="/logout" className="max-w-full">
|
||||
Logout
|
||||
</SecondaryLink> */}
|
||||
</Popover.Panel>
|
||||
</Transition.Child>
|
||||
</Transition.Root>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNavIcon({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 overflow-visible stroke-slate-300"
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<path
|
||||
d="M0 1H14M0 7H14M0 13H14"
|
||||
className={cn("origin-center transition", open && "scale-90 opacity-0")}
|
||||
/>
|
||||
<path
|
||||
d="M2 2L12 12M12 2L2 12"
|
||||
className={cn("origin-center transition", !open && "scale-90 opacity-0")}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { Fragment, useState } from "react";
|
||||
import simplur from "simplur";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
PopoverSectionHeader,
|
||||
} from "../primitives/Popover";
|
||||
|
||||
export function ProjectsMenu({ matches }: { matches: RouteMatch[] }) {
|
||||
export function ProjectsMenu({ matches }: { matches: UIMatch[] }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const organizations = useOrganizations(matches);
|
||||
const isNewOrgPage = useIsNewOrganizationPage(matches);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
export const variantClasses = {
|
||||
info: {
|
||||
@@ -51,8 +52,16 @@ export const variantClasses = {
|
||||
textColor: "text-blue-200",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
pending: {
|
||||
className: "border-blue-400/20 bg-blue-800/30",
|
||||
icon: <Spinner className="h-5 w-5 shrink-0 " />,
|
||||
textColor: "text-blue-300",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type CalloutVariant = keyof typeof variantClasses;
|
||||
|
||||
export function Callout({
|
||||
children,
|
||||
className,
|
||||
@@ -63,7 +72,7 @@ export function Callout({
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
variant: keyof typeof variantClasses;
|
||||
variant: CalloutVariant;
|
||||
to?: string;
|
||||
}) {
|
||||
const variantDefinition = variantClasses[variant];
|
||||
|
||||
@@ -2,8 +2,17 @@ import type { z } from "zod";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function FormError({ children, id }: { children: React.ReactNode; id?: string }) {
|
||||
export function FormError({
|
||||
children,
|
||||
id,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
id?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{children && (
|
||||
@@ -11,7 +20,7 @@ export function FormError({ children, id }: { children: React.ReactNode; id?: st
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="flex items-start gap-0.5"
|
||||
className={cn("flex items-start gap-0.5", className)}
|
||||
>
|
||||
<NamedIcon name="error" className="h-4 w-4 shrink-0 justify-start" />
|
||||
<Paragraph id={id} variant="extra-small" className="text-rose-500">
|
||||
|
||||
@@ -68,6 +68,8 @@ import { Spinner } from "./Spinner";
|
||||
import { SaplingIcon } from "~/assets/icons/SaplingIcon";
|
||||
import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon";
|
||||
import { OneTreeIcon } from "~/assets/icons/OneTreeIcon";
|
||||
import { tablerIcons } from "~/utils/tablerIcons";
|
||||
import tablerSpritePath from "./tabler-sprite.svg";
|
||||
|
||||
const icons = {
|
||||
account: (className: string) => <UserCircleIcon className={cn("text-slate-400", className)} />,
|
||||
@@ -215,7 +217,15 @@ export function NamedIcon({
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Icon ${name} not found`);
|
||||
if (tablerIcons.has("tabler-" + name)) {
|
||||
return <TablerIcon name={"tabler-" + name} className={className} />;
|
||||
} else if (name.startsWith("tabler-") && tablerIcons.has(name)) {
|
||||
return <TablerIcon name={name} className={className} />;
|
||||
}
|
||||
|
||||
if (name === "supabase-management") {
|
||||
return <NamedIcon name="supabase" className={className} />;
|
||||
}
|
||||
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
@@ -247,3 +257,11 @@ export function NamedIconInBox({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TablerIcon({ name, className }: { name: string; className?: string }) {
|
||||
return (
|
||||
<svg className={cn("stroke-[1.5]", className)}>
|
||||
<use xlinkHref={`${tablerSpritePath}#${name}`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const variations = {
|
||||
container: "flex items-center gap-x-1.5 rounded hover:bg-slate-850 pr-1 py-[0.1rem] pl-1.5",
|
||||
root: "h-3 w-6",
|
||||
thumb: "h-2.5 w-2.5 data-[state=checked]:translate-x-2.5 data-[state=unchecked]:translate-x-0",
|
||||
text: "text-xs text-slate-400 group-hover:text-slate-200 mt-0.5",
|
||||
text: "text-xs text-slate-400 group-hover:text-slate-200 hover:cursor-pointer",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import toast, { Toaster, resolveValue, useToasterStore } from "react-hot-toast";
|
||||
import { Toaster, toast } from "sonner";
|
||||
|
||||
import { useTypedLoaderData } from "remix-typedjson";
|
||||
import { loader } from "~/root";
|
||||
import { useEffect } from "react";
|
||||
@@ -11,79 +11,55 @@ const permanentToastDuration = 60 * 60 * 24 * 1000;
|
||||
|
||||
export function Toast() {
|
||||
const { toastMessage } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!toastMessage) {
|
||||
return;
|
||||
}
|
||||
const { message, type, options } = toastMessage;
|
||||
|
||||
switch (type) {
|
||||
case "success":
|
||||
toast.success(message, {
|
||||
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
|
||||
});
|
||||
break;
|
||||
case "error":
|
||||
toast.error(message, {
|
||||
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${type} is not handled`);
|
||||
}
|
||||
toast.custom((t) => <ToastUI variant={type} message={message} t={t as string} />, {
|
||||
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
|
||||
});
|
||||
}, [toastMessage]);
|
||||
|
||||
return <Toaster />;
|
||||
}
|
||||
|
||||
export function ToastUI({
|
||||
variant,
|
||||
message,
|
||||
t,
|
||||
toastWidth = 356, // Default width, matches what sonner provides by default
|
||||
}: {
|
||||
variant: "error" | "success";
|
||||
message: string;
|
||||
t: string;
|
||||
toastWidth?: string | number;
|
||||
}) {
|
||||
return (
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
toastOptions={{
|
||||
success: {
|
||||
icon: <CheckCircleIcon className="h-6 w-6 text-green-600" />,
|
||||
},
|
||||
error: {
|
||||
icon: <ExclamationCircleIcon className="h-6 w-6 text-rose-600" />,
|
||||
},
|
||||
<div
|
||||
className={`self-end rounded-lg border border-slate-750 bg-midnight-900 shadow-md`}
|
||||
style={{
|
||||
width: toastWidth,
|
||||
}}
|
||||
>
|
||||
{(t) => (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="flex gap-2 rounded-lg border border-slate-750 bg-no-repeat p-4 text-bright shadow-md"
|
||||
style={{
|
||||
opacity: t.visible ? 1 : 0,
|
||||
background:
|
||||
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
|
||||
}}
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
animate={t.visible ? "visible" : "hidden"}
|
||||
variants={{
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.15,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t.icon}
|
||||
{resolveValue(t.message, t)}
|
||||
<button className="p-1" onClick={() => toast.dismiss(t.id)}>
|
||||
<XMarkIcon className="h-4 w-4 text-bright" />
|
||||
</button>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</Toaster>
|
||||
<div
|
||||
className="flex w-full gap-2 rounded-lg bg-no-repeat p-4 text-bright"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
|
||||
}}
|
||||
>
|
||||
{variant === "success" ? (
|
||||
<CheckCircleIcon className="h-6 w-6 text-green-600" />
|
||||
) : (
|
||||
<ExclamationCircleIcon className="h-6 w-6 text-rose-600" />
|
||||
)}
|
||||
{message}
|
||||
<button className="ms-auto p-1" onClick={() => toast.dismiss(t)}>
|
||||
<XMarkIcon className="h-4 w-4 text-bright" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.0 MiB |
@@ -313,7 +313,8 @@ function RerunPopover({
|
||||
|
||||
const [form, { successRedirect }] = useForm({
|
||||
id: "rerun",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
@@ -386,7 +387,8 @@ export function CancelRun({ runId }: { runId: string }) {
|
||||
|
||||
const [form, { redirectUrl }] = useForm({
|
||||
id: "cancel-run",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: cancelSchema });
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "../primitives/Table";
|
||||
import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
|
||||
import { TaskStatusIcon } from "./TaskStatus";
|
||||
import { ClientOnly } from "remix-utils";
|
||||
import { ClientOnly } from "remix-utils/client-only";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import type { DetailedTask } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route";
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import { NamedIcon, iconNames } from "../primitives/NamedIcon";
|
||||
import { tablerIcons } from "~/utils/tablerIcons";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
|
||||
const meta: Meta<typeof NamedIcons> = {
|
||||
title: "Icons",
|
||||
@@ -25,17 +27,35 @@ export const Basic: Story = {
|
||||
|
||||
function NamedIcons() {
|
||||
return (
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
{iconNames
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((iconName) => (
|
||||
<div key={iconName} className="flex items-center gap-2">
|
||||
<div>
|
||||
<NamedIcon name={iconName} className={"h-6 w-6"} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Header1 spacing>Internal</Header1>
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
{iconNames
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((iconName) => (
|
||||
<div key={iconName} className="flex items-center gap-2">
|
||||
<div>
|
||||
<NamedIcon name={iconName} className={"h-6 w-6"} />
|
||||
</div>
|
||||
<span className="text-xs text-dimmed">{iconName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Header1 spacing>Tabler</Header1>
|
||||
<div className="grid grid-cols-8 gap-4">
|
||||
{Array.from(tablerIcons).map((iconName) => (
|
||||
<div key={iconName} className="flex items-center gap-2">
|
||||
<div>
|
||||
<NamedIcon name={iconName} className={"h-6 w-6 text-indigo-500"} />
|
||||
</div>
|
||||
<span className="text-xs text-dimmed">{iconName}</span>
|
||||
</div>
|
||||
<span className="text-xs text-dimmed">{iconName}</span>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { ToastUI } from "../primitives/Toast";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/Toast",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Collection>;
|
||||
|
||||
export const Toasts: Story = {
|
||||
render: () => <Collection />,
|
||||
};
|
||||
|
||||
function Collection() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-4 p-4">
|
||||
<ToastUI variant="success" message="Success UI" t="-" />
|
||||
<ToastUI variant="error" message="Error UI" t="-" />
|
||||
<br />
|
||||
<Button
|
||||
variant="primary/large"
|
||||
onClick={() =>
|
||||
toast.custom((t) => <ToastUI variant="success" message="Success" t={t as string} />, {
|
||||
duration: Infinity, // Prevents auto-dismissal for demo purposes
|
||||
})
|
||||
}
|
||||
>
|
||||
Success
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary/large"
|
||||
onClick={() =>
|
||||
toast.custom((t) => <ToastUI variant="error" message="Error" t={t as string} />, {
|
||||
duration: Infinity,
|
||||
})
|
||||
}
|
||||
>
|
||||
Error
|
||||
</Button>
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,3 +6,6 @@ export const MAX_CONCURRENT_RUNS_LIMIT = 20;
|
||||
export const PREPROCESS_RETRY_LIMIT = 2;
|
||||
export const EXECUTE_JOB_RETRY_LIMIT = 10;
|
||||
export const MAX_RUN_YIELDED_EXECUTIONS = 100;
|
||||
export const RUN_CHUNK_EXECUTION_BUFFER = 350;
|
||||
export const MAX_RUN_CHUNK_EXECUTION_LIMIT = 120000; // 2 minutes
|
||||
export const RESPONSE_TIMEOUT_STATUS_CODES = [408, 504];
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { H } from "@highlight-run/node";
|
||||
import type { DataFunctionArgs, EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
|
||||
import { Response } from "@remix-run/node"; // or cloudflare/deno
|
||||
import {
|
||||
createReadableStreamFromReadable,
|
||||
type DataFunctionArgs,
|
||||
type EntryContext,
|
||||
} from "@remix-run/node"; // or cloudflare/deno
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { parseAcceptLanguage } from "intl-parse-accept-language";
|
||||
import isbot from "isbot";
|
||||
@@ -36,7 +39,7 @@ export default function handleRequest(
|
||||
// response to render before sending it to the client. This
|
||||
// ensures that bots can see the full page content.
|
||||
if (isbot(request.headers.get("user-agent"))) {
|
||||
return serveTheBots(
|
||||
return handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
@@ -46,7 +49,7 @@ export default function handleRequest(
|
||||
);
|
||||
}
|
||||
|
||||
return serveBrowsers(
|
||||
return handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
@@ -56,7 +59,7 @@ export default function handleRequest(
|
||||
);
|
||||
}
|
||||
|
||||
function serveTheBots(
|
||||
function handleBotRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
@@ -65,76 +68,97 @@ function serveTheBots(
|
||||
platform: OperatingSystemPlatform
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<OperatingSystemContextProvider platform={platform}>
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />
|
||||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>,
|
||||
{
|
||||
// Use onAllReady to wait for the entire document to be ready
|
||||
onAllReady() {
|
||||
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
|
||||
let body = new PassThrough();
|
||||
pipe(body);
|
||||
resolve(
|
||||
new Response(body, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders,
|
||||
})
|
||||
);
|
||||
},
|
||||
onShellError(err: unknown) {
|
||||
reject(err);
|
||||
},
|
||||
}
|
||||
);
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
function serveBrowsers(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
locales: string[],
|
||||
platform: OperatingSystemPlatform
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
let shellReady = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<OperatingSystemContextProvider platform={platform}>
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>,
|
||||
{
|
||||
// use onShellReady to wait until a suspense boundary is triggered
|
||||
onShellReady() {
|
||||
shellReady = true;
|
||||
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
|
||||
let body = new PassThrough();
|
||||
pipe(body);
|
||||
resolve(
|
||||
new Response(body, {
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(err: unknown) {
|
||||
reject(err);
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
didError = true;
|
||||
if (shellReady) {
|
||||
logError(error, request);
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
function handleBrowserRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
locales: string[],
|
||||
platform: OperatingSystemPlatform
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<OperatingSystemContextProvider platform={platform}>
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />
|
||||
</LocaleContextProvider>
|
||||
</OperatingSystemContextProvider>,
|
||||
{
|
||||
onShellReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ const EnvironmentSchema = z.object({
|
||||
EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
|
||||
WORKER_ENABLED: z.string().default("true"),
|
||||
EXECUTION_WORKER_ENABLED: z.string().default("true"),
|
||||
GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { MatchedProject, useOptionalProject } from "./useProject";
|
||||
import { useUser } from "./useUser";
|
||||
|
||||
export type ProjectJobEnvironment = MatchedProject["environments"][number];
|
||||
|
||||
export function useEnvironments(matches?: RouteMatch[]) {
|
||||
export function useEnvironments(matches?: UIMatch[]) {
|
||||
const project = useOptionalProject(matches);
|
||||
if (!project) return;
|
||||
|
||||
return project.environments;
|
||||
}
|
||||
|
||||
export function useDevEnvironment(matches?: RouteMatch[]) {
|
||||
export function useDevEnvironment(matches?: UIMatch[]) {
|
||||
const user = useUser();
|
||||
const environments = useEnvironments(matches);
|
||||
if (!environments) return;
|
||||
|
||||
return environments.find((environment) => environment.type === "DEVELOPMENT");
|
||||
return environments.find(
|
||||
(environment) => environment.type === "DEVELOPMENT" && environment.userId === user.id
|
||||
);
|
||||
}
|
||||
|
||||
export function useProdEnvironment(matches?: RouteMatch[]) {
|
||||
export function useProdEnvironment(matches?: UIMatch[]) {
|
||||
const environments = useEnvironments(matches);
|
||||
if (!environments) return;
|
||||
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { ProjectJob } from "./useJobs";
|
||||
import { useTextFilter } from "./useTextFilter";
|
||||
import { useToggleFilter } from "./useToggleFilter";
|
||||
|
||||
export function useFilterJobs(jobs: ProjectJob[]) {
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<ProjectJob>({
|
||||
export function useFilterJobs(jobs: ProjectJob[], onlyActiveJobs = false) {
|
||||
const toggleFilterRes = useToggleFilter<ProjectJob>({
|
||||
items: jobs,
|
||||
filter: (job, onlyActiveJobs) => {
|
||||
if (onlyActiveJobs && job.status !== "ACTIVE") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
defaultValue: onlyActiveJobs,
|
||||
});
|
||||
|
||||
const textFilterRes = useTextFilter<ProjectJob>({
|
||||
items: toggleFilterRes.filteredItems,
|
||||
filter: (job, text) => {
|
||||
if (job.slug.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
if (job.title.toLowerCase().includes(text.toLowerCase())) return true;
|
||||
@@ -24,5 +36,11 @@ export function useFilterJobs(jobs: ProjectJob[]) {
|
||||
},
|
||||
});
|
||||
|
||||
return { filterText, setFilterText, filteredItems };
|
||||
return {
|
||||
filteredItems: textFilterRes.filteredItems,
|
||||
filterText: textFilterRes.filterText,
|
||||
setFilterText: textFilterRes.setFilterText,
|
||||
onlyActiveJobs: toggleFilterRes.isToggleActive,
|
||||
setOnlyActiveJobs: toggleFilterRes.setToggleActive,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route";
|
||||
@@ -6,7 +6,7 @@ import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type MatchedClient = UseDataFunctionReturn<typeof loader>["client"];
|
||||
|
||||
export function useOptionalIntegrationClient(matches?: RouteMatch[]) {
|
||||
export function useOptionalIntegrationClient(matches?: UIMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam",
|
||||
matches,
|
||||
@@ -15,7 +15,7 @@ export function useOptionalIntegrationClient(matches?: RouteMatch[]) {
|
||||
return routeMatch?.client;
|
||||
}
|
||||
|
||||
export function useIntegrationClient(matches?: RouteMatch[]) {
|
||||
export function useIntegrationClient(matches?: UIMatch[]) {
|
||||
const integration = useOptionalIntegrationClient(matches);
|
||||
invariant(integration, "Integration must be defined");
|
||||
return integration;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RouteMatch, useMatches } from "@remix-run/react";
|
||||
import { UIMatch, useMatches } from "@remix-run/react";
|
||||
|
||||
export function useIsProjectChildPage(matches?: RouteMatch[]) {
|
||||
export function useIsProjectChildPage(matches?: UIMatch[]) {
|
||||
if (!matches) {
|
||||
matches = useMatches();
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
|
||||
import { useChanged } from "./useChanged";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
|
||||
|
||||
export const jobMatchId =
|
||||
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
|
||||
export function useOptionalJob(matches?: RouteMatch[]) {
|
||||
export function useOptionalJob(matches?: UIMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: jobMatchId,
|
||||
matches,
|
||||
@@ -22,7 +22,7 @@ export function useOptionalJob(matches?: RouteMatch[]) {
|
||||
return routeMatch.projectJobs.find((j) => j.id === routeMatch.job.id);
|
||||
}
|
||||
|
||||
export function useJob(matches?: RouteMatch[]) {
|
||||
export function useJob(matches?: UIMatch[]) {
|
||||
const job = useOptionalJob(matches);
|
||||
invariant(job, "Job must be defined");
|
||||
return job;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type ProjectJob = UseDataFunctionReturn<typeof loader>["projectJobs"][number];
|
||||
@@ -12,7 +12,7 @@ export const jobsMatchId =
|
||||
// This is only used in the JobsMenu component, which is the breadcrumb job list dropdown.
|
||||
// This dropdown is only shown once you have selected a job, so we can assume that
|
||||
// the route above has loaded and we can use the data from it.
|
||||
export function useOptionalJobs(matches?: RouteMatch[]) {
|
||||
export function useOptionalJobs(matches?: UIMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: jobsMatchId,
|
||||
matches,
|
||||
@@ -21,7 +21,7 @@ export function useOptionalJobs(matches?: RouteMatch[]) {
|
||||
return routeMatch?.projectJobs;
|
||||
}
|
||||
|
||||
export function useJobs(matches?: RouteMatch[]) {
|
||||
export function useJobs(matches?: UIMatch[]) {
|
||||
const jobs = useOptionalJobs(matches);
|
||||
invariant(jobs, "Jobs must be defined");
|
||||
return jobs;
|
||||
|
||||
@@ -4,12 +4,12 @@ import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/r
|
||||
import type { loader as appLoader } from "~/routes/_app/route";
|
||||
import { hydrateObject, useMatchesData } from "~/utils";
|
||||
import { useChanged } from "./useChanged";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type MatchedOrganization = UseDataFunctionReturn<typeof appLoader>["organizations"][number];
|
||||
|
||||
export function useOptionalOrganizations(matches?: RouteMatch[]) {
|
||||
export function useOptionalOrganizations(matches?: UIMatch[]) {
|
||||
const data = useTypedMatchesData<typeof appLoader>({
|
||||
id: "routes/_app",
|
||||
matches,
|
||||
@@ -17,13 +17,13 @@ export function useOptionalOrganizations(matches?: RouteMatch[]) {
|
||||
return data?.organizations;
|
||||
}
|
||||
|
||||
export function useOrganizations(matches?: RouteMatch[]) {
|
||||
export function useOrganizations(matches?: UIMatch[]) {
|
||||
const orgs = useOptionalOrganizations(matches);
|
||||
invariant(orgs, "No organizations found in loader.");
|
||||
return orgs;
|
||||
}
|
||||
|
||||
export function useOptionalOrganization(matches?: RouteMatch[]) {
|
||||
export function useOptionalOrganization(matches?: UIMatch[]) {
|
||||
const orgs = useOptionalOrganizations(matches);
|
||||
const org = useTypedMatchesData<typeof orgLoader>({
|
||||
id: "routes/_app.orgs.$organizationSlug",
|
||||
@@ -37,13 +37,13 @@ export function useOptionalOrganization(matches?: RouteMatch[]) {
|
||||
return orgs.find((o) => o.id === org.organization.id);
|
||||
}
|
||||
|
||||
export function useOrganization(matches?: RouteMatch[]) {
|
||||
export function useOrganization(matches?: UIMatch[]) {
|
||||
const org = useOptionalOrganization(matches);
|
||||
invariant(org, "No organization found in loader.");
|
||||
return org;
|
||||
}
|
||||
|
||||
export function useIsNewOrganizationPage(matches?: RouteMatch[]): boolean {
|
||||
export function useIsNewOrganizationPage(matches?: UIMatch[]): boolean {
|
||||
const data = useTypedMatchesData<any>({
|
||||
id: "routes/_app.orgs.new",
|
||||
matches,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
|
||||
@@ -9,7 +9,7 @@ export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
|
||||
|
||||
export const projectMatchId = "routes/_app.orgs.$organizationSlug.projects.$projectParam";
|
||||
|
||||
export function useOptionalProject(matches?: RouteMatch[]) {
|
||||
export function useOptionalProject(matches?: UIMatch[]) {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: projectMatchId,
|
||||
matches,
|
||||
@@ -18,7 +18,7 @@ export function useOptionalProject(matches?: RouteMatch[]) {
|
||||
return routeMatch?.project;
|
||||
}
|
||||
|
||||
export function useProject(matches?: RouteMatch[]) {
|
||||
export function useProject(matches?: UIMatch[]) {
|
||||
const project = useOptionalProject(matches);
|
||||
invariant(project, "Project must be defined");
|
||||
return project;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { useEventSource } from "remix-utils/sse/react";
|
||||
import { projectPath, projectStreamingPath } from "~/utils/pathBuilder";
|
||||
import { useProject } from "./useProject";
|
||||
import { useOrganization } from "./useOrganizations";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route";
|
||||
@@ -7,7 +7,7 @@ import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type MatchedRun = UseDataFunctionReturn<typeof runLoader>["run"];
|
||||
|
||||
export function useOptionalRun(matches?: RouteMatch[]) {
|
||||
export function useOptionalRun(matches?: UIMatch[]) {
|
||||
const project = useOptionalProject(matches);
|
||||
const routeMatch = useTypedMatchesData<typeof runLoader>({
|
||||
id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam",
|
||||
@@ -21,7 +21,7 @@ export function useOptionalRun(matches?: RouteMatch[]) {
|
||||
return routeMatch.run;
|
||||
}
|
||||
|
||||
export function useRun(matches?: RouteMatch[]) {
|
||||
export function useRun(matches?: UIMatch[]) {
|
||||
const run = useOptionalRun(matches);
|
||||
invariant(run, "Run must be present");
|
||||
return run;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type ToggleFilterProps<T> = {
|
||||
items: T[];
|
||||
filter: (item: T, isToggleActive: boolean) => boolean;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
|
||||
export function useToggleFilter<T>({ items, filter, defaultValue = false }: ToggleFilterProps<T>) {
|
||||
const [isToggleActive, setToggleActive] = useState(defaultValue);
|
||||
|
||||
const filteredItems = useMemo<T[]>(() => {
|
||||
return items.filter((item) => filter(item, isToggleActive));
|
||||
}, [items, isToggleActive]);
|
||||
|
||||
return {
|
||||
isToggleActive,
|
||||
setToggleActive,
|
||||
filteredItems,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouteMatch, useMatches } from "@remix-run/react";
|
||||
import { UIMatch, useMatches } from "@remix-run/react";
|
||||
import { RemixSerializedType, UseDataFunctionReturn, deserializeRemix } from "remix-typedjson";
|
||||
|
||||
type AppData = any;
|
||||
@@ -8,7 +8,7 @@ function useTypedDataFromMatches<T = AppData>({
|
||||
matches,
|
||||
}: {
|
||||
id: string;
|
||||
matches: RouteMatch[];
|
||||
matches: UIMatch[];
|
||||
}): UseDataFunctionReturn<T> | undefined {
|
||||
const match = matches.find((m) => m.id === id);
|
||||
return useTypedMatchData<T>(match);
|
||||
@@ -19,7 +19,7 @@ export function useTypedMatchesData<T = AppData>({
|
||||
matches,
|
||||
}: {
|
||||
id: string;
|
||||
matches?: RouteMatch[];
|
||||
matches?: UIMatch[];
|
||||
}): UseDataFunctionReturn<T> | undefined {
|
||||
if (!matches) {
|
||||
matches = useMatches();
|
||||
@@ -29,7 +29,7 @@ export function useTypedMatchesData<T = AppData>({
|
||||
}
|
||||
|
||||
export function useTypedMatchData<T = AppData>(
|
||||
match: RouteMatch | undefined
|
||||
match: UIMatch | undefined
|
||||
): UseDataFunctionReturn<T> | undefined {
|
||||
if (!match) {
|
||||
return undefined;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { User } from "~/models/user.server";
|
||||
import { useMatchesData } from "~/utils";
|
||||
import { useChanged } from "./useChanged";
|
||||
import { RouteMatch } from "@remix-run/react";
|
||||
import { UIMatch } from "@remix-run/react";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
import { loader } from "~/root";
|
||||
|
||||
export function useOptionalUser(matches?: RouteMatch[]): User | undefined {
|
||||
export function useOptionalUser(matches?: UIMatch[]): User | undefined {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "root",
|
||||
matches,
|
||||
@@ -14,7 +14,7 @@ export function useOptionalUser(matches?: RouteMatch[]): User | undefined {
|
||||
return routeMatch?.user ?? undefined;
|
||||
}
|
||||
|
||||
export function useUser(matches?: RouteMatch[]): User {
|
||||
export function useUser(matches?: UIMatch[]): User {
|
||||
const maybeUser = useOptionalUser(matches);
|
||||
if (!maybeUser) {
|
||||
throw new Error(
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { RuntimeEnvironmentType, type RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import { prisma } from "~/db.server";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
const apiKeyId = customAlphabet(
|
||||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
12
|
||||
);
|
||||
|
||||
type RegenerateAPIKeyInput = {
|
||||
userId: string;
|
||||
environmentId: string;
|
||||
};
|
||||
|
||||
export async function regenerateApiKey({ userId, environmentId }: RegenerateAPIKeyInput) {
|
||||
const environment = await prisma.runtimeEnvironment.findUnique({
|
||||
where: {
|
||||
id: environmentId,
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
// check if the user is part of the org
|
||||
const organization = await prisma.organization.findFirst({
|
||||
where: {
|
||||
id: environment.organization.id,
|
||||
members: { some: { userId } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("User does not have permission to regenerate API key");
|
||||
}
|
||||
|
||||
// check if it is the user's dev environment
|
||||
if (environment.type === RuntimeEnvironmentType.DEVELOPMENT) {
|
||||
if (!environment.orgMemberId) {
|
||||
throw new Error("User does not have permission to regenerate API key");
|
||||
}
|
||||
|
||||
const orgMember = await prisma.orgMember.findFirst({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
userId: userId,
|
||||
id: environment.orgMemberId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!orgMember) {
|
||||
throw new Error("User does not have permission to regenerate API key");
|
||||
}
|
||||
}
|
||||
|
||||
// generate and store new keys
|
||||
const newApiKey = createApiKeyForEnv(environment.type);
|
||||
const newPkApiKey = createPkApiKeyForEnv(environment.type);
|
||||
|
||||
const updatedEnviroment = await prisma.runtimeEnvironment.update({
|
||||
data: {
|
||||
apiKey: newApiKey,
|
||||
pkApiKey: newPkApiKey,
|
||||
},
|
||||
where: {
|
||||
id: environmentId,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedEnviroment;
|
||||
}
|
||||
|
||||
export function createApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
|
||||
return `tr_${envSlug(envType)}_${apiKeyId(20)}`;
|
||||
}
|
||||
|
||||
export function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
|
||||
return `pk_${envSlug(envType)}_${apiKeyId(20)}`;
|
||||
}
|
||||
|
||||
export function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
switch (environmentType) {
|
||||
case "DEVELOPMENT": {
|
||||
return "dev";
|
||||
}
|
||||
case "PRODUCTION": {
|
||||
return "prod";
|
||||
}
|
||||
case "STAGING": {
|
||||
return "stg";
|
||||
}
|
||||
case "PREVIEW": {
|
||||
return "prev";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RESPONSE_TIMEOUT_STATUS_CODES } from "~/consts";
|
||||
import { prisma } from "~/db.server";
|
||||
import { Prettify } from "~/lib.es5";
|
||||
|
||||
@@ -18,3 +19,14 @@ export async function findEndpoint(id: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function detectResponseIsTimeout(response?: Response) {
|
||||
if (!response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
RESPONSE_TIMEOUT_STATUS_CODES.includes(response.status) ||
|
||||
response.headers.get("x-vercel-error") === "FUNCTION_INVOCATION_TIMEOUT"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const IndexEndpointStatsSchema = z.object({
|
||||
jobs: z.number(),
|
||||
sources: z.number(),
|
||||
dynamicTriggers: z.number(),
|
||||
dynamicSchedules: z.number(),
|
||||
});
|
||||
|
||||
export type IndexEndpointStats = z.infer<typeof IndexEndpointStatsSchema>;
|
||||
|
||||
export function parseEndpointIndexStats(stats: unknown): IndexEndpointStats {
|
||||
return IndexEndpointStatsSchema.parse(stats);
|
||||
}
|
||||
@@ -2,27 +2,6 @@ import { JobRun, JobRunExecution } from "@trigger.dev/database";
|
||||
import { PrismaClientOrTransaction } from "~/db.server";
|
||||
import { executionWorker } from "~/services/worker.server";
|
||||
|
||||
export async function enqueueRunExecutionV1(
|
||||
execution: JobRunExecution,
|
||||
queueId: string,
|
||||
concurrency: number,
|
||||
tx: PrismaClientOrTransaction,
|
||||
runAt?: Date
|
||||
) {
|
||||
const job = await executionWorker.enqueue(
|
||||
"performRunExecution",
|
||||
{
|
||||
id: execution.id,
|
||||
},
|
||||
{
|
||||
queueName: `job:queue:${queueId}`,
|
||||
tx,
|
||||
runAt,
|
||||
jobKey: `execution:${execution.runId}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export type EnqueueRunExecutionV2Options = {
|
||||
runAt?: Date;
|
||||
resumeTaskId?: string;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { customAlphabet } from "nanoid";
|
||||
import slug from "slug";
|
||||
import { prisma, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { createProject } from "./project.server";
|
||||
import { createApiKeyForEnv, createPkApiKeyForEnv, envSlug } from "./api-key.server";
|
||||
|
||||
export type { Organization };
|
||||
|
||||
@@ -157,28 +158,3 @@ export async function createEnvironment(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
|
||||
return `tr_${envSlug(envType)}_${apiKeyId(20)}`;
|
||||
}
|
||||
|
||||
function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
|
||||
return `pk_${envSlug(envType)}_${apiKeyId(20)}`;
|
||||
}
|
||||
|
||||
function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
switch (environmentType) {
|
||||
case "DEVELOPMENT": {
|
||||
return "dev";
|
||||
}
|
||||
case "PRODUCTION": {
|
||||
return "prod";
|
||||
}
|
||||
case "STAGING": {
|
||||
return "stg";
|
||||
}
|
||||
case "PREVIEW": {
|
||||
return "prev";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Task, TaskAttempt } from "@trigger.dev/database";
|
||||
import type { JobRun, Task, TaskAttempt } from "@trigger.dev/database";
|
||||
import { CachedTask, ServerTask } from "@trigger.dev/core";
|
||||
|
||||
export type TaskWithAttempts = Task & { attempts: TaskAttempt[] };
|
||||
export type TaskWithAttempts = Task & { attempts: TaskAttempt[]; run: JobRun };
|
||||
|
||||
export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask {
|
||||
return {
|
||||
@@ -24,6 +24,7 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
|
||||
idempotencyKey: task.idempotencyKey,
|
||||
operation: task.operation,
|
||||
callbackUrl: task.callbackUrl,
|
||||
forceYield: task.run.forceYieldImmediately,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { run as graphileRun, parseCronItems } from "graphile-worker";
|
||||
import omit from "lodash.omit";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { workerLogger as logger } from "~/services/logger.server";
|
||||
|
||||
export interface MessageCatalogSchema {
|
||||
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
|
||||
@@ -81,6 +81,18 @@ export type ZodWorkerDequeueOptions = {
|
||||
tx?: PrismaClientOrTransaction;
|
||||
};
|
||||
|
||||
const CLEANUP_TASK_NAME = "__cleanupOldJobs";
|
||||
const REPORTER_TASK_NAME = "__reporter";
|
||||
|
||||
export type ZodWorkerCleanupOptions = {
|
||||
frequencyExpression: string; // cron expression
|
||||
ttl: number;
|
||||
maxCount: number;
|
||||
taskOptions?: CronItemOptions;
|
||||
};
|
||||
|
||||
type ZodWorkerReporter = (event: string, properties: Record<string, any>) => Promise<void>;
|
||||
|
||||
export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
|
||||
name: string;
|
||||
runnerOptions: RunnerOptions;
|
||||
@@ -88,6 +100,9 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
|
||||
schema: TMessageCatalog;
|
||||
tasks: ZodTasks<TMessageCatalog>;
|
||||
recurringTasks?: ZodRecurringTasks;
|
||||
cleanup?: ZodWorkerCleanupOptions;
|
||||
reporter?: ZodWorkerReporter;
|
||||
shutdownTimeoutInMs?: number;
|
||||
};
|
||||
|
||||
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
@@ -98,6 +113,10 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
#tasks: ZodTasks<TMessageCatalog>;
|
||||
#recurringTasks?: ZodRecurringTasks;
|
||||
#runner?: GraphileRunner;
|
||||
#cleanup: ZodWorkerCleanupOptions | undefined;
|
||||
#reporter?: ZodWorkerReporter;
|
||||
#shutdownTimeoutInMs?: number;
|
||||
#shuttingDown = false;
|
||||
|
||||
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
|
||||
this.#name = options.name;
|
||||
@@ -106,6 +125,9 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
this.#runnerOptions = options.runnerOptions;
|
||||
this.#tasks = options.tasks;
|
||||
this.#recurringTasks = options.recurringTasks;
|
||||
this.#cleanup = options.cleanup;
|
||||
this.#reporter = options.reporter;
|
||||
this.#shutdownTimeoutInMs = options.shutdownTimeoutInMs ?? 60000; // default to 60 seconds
|
||||
}
|
||||
|
||||
get graphileWorkerSchema() {
|
||||
@@ -125,6 +147,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
|
||||
this.#runner = await graphileRun({
|
||||
...this.#runnerOptions,
|
||||
noHandleSignals: true,
|
||||
taskList: this.#createTaskListFromTasks(),
|
||||
parsedCronItems,
|
||||
});
|
||||
@@ -181,9 +204,34 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
this.#logDebug("stop");
|
||||
});
|
||||
|
||||
process.on("SIGTERM", this._handleSignal.bind(this));
|
||||
process.on("SIGINT", this._handleSignal.bind(this));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleSignal(signal: string) {
|
||||
if (this.#shuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#shuttingDown = true;
|
||||
|
||||
if (this.#shutdownTimeoutInMs) {
|
||||
setTimeout(() => {
|
||||
this.#logDebug("Shutdown timeout reached, exiting process");
|
||||
|
||||
process.exit(0);
|
||||
}, this.#shutdownTimeoutInMs);
|
||||
}
|
||||
|
||||
this.#logDebug(`Received ${signal}, shutting down zodWorker...`);
|
||||
|
||||
this.stop().finally(() => {
|
||||
this.#logDebug("zodWorker stopped");
|
||||
});
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
await this.#runner?.stop();
|
||||
}
|
||||
@@ -337,12 +385,45 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
taskList[key] = task;
|
||||
}
|
||||
|
||||
if (this.#cleanup) {
|
||||
const task: Task = (payload, helpers) => {
|
||||
return this.#handleCleanup(payload, helpers);
|
||||
};
|
||||
|
||||
taskList[CLEANUP_TASK_NAME] = task;
|
||||
}
|
||||
|
||||
if (this.#reporter) {
|
||||
const task: Task = (payload, helpers) => {
|
||||
return this.#handleReporter(payload, helpers);
|
||||
};
|
||||
|
||||
taskList[REPORTER_TASK_NAME] = task;
|
||||
}
|
||||
|
||||
return taskList;
|
||||
}
|
||||
|
||||
#createCronItemsFromRecurringTasks() {
|
||||
const cronItems: CronItem[] = [];
|
||||
|
||||
if (this.#cleanup) {
|
||||
cronItems.push({
|
||||
pattern: this.#cleanup.frequencyExpression,
|
||||
identifier: CLEANUP_TASK_NAME,
|
||||
task: CLEANUP_TASK_NAME,
|
||||
options: this.#cleanup.taskOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.#reporter) {
|
||||
cronItems.push({
|
||||
pattern: "50 * * * *", // Every hour at 50 minutes past the hour
|
||||
identifier: REPORTER_TASK_NAME,
|
||||
task: REPORTER_TASK_NAME,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.#recurringTasks) {
|
||||
return cronItems;
|
||||
}
|
||||
@@ -434,6 +515,112 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
|
||||
}
|
||||
}
|
||||
|
||||
async #handleCleanup(rawPayload: unknown, helpers: JobHelpers): Promise<void> {
|
||||
if (!this.#cleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const job = helpers.job;
|
||||
|
||||
logger.debug("Received cleanup task", {
|
||||
payload: rawPayload,
|
||||
job,
|
||||
});
|
||||
|
||||
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error(
|
||||
`Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}`
|
||||
);
|
||||
}
|
||||
|
||||
const payload = parsedPayload.data;
|
||||
|
||||
// Add the this.#cleanup.ttl to the payload._cron.ts
|
||||
const expirationDate = new Date(payload._cron.ts.getTime() - this.#cleanup.ttl);
|
||||
|
||||
logger.debug("Cleaning up old jobs", {
|
||||
expirationDate,
|
||||
payload,
|
||||
});
|
||||
|
||||
const rawResults = await this.#prisma.$queryRawUnsafe(
|
||||
`WITH rows AS (SELECT id FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts LIMIT $2 FOR UPDATE) DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE id IN (SELECT id FROM rows) RETURNING id`,
|
||||
expirationDate,
|
||||
this.#cleanup.maxCount
|
||||
);
|
||||
|
||||
const results = Array.isArray(rawResults) ? rawResults : [];
|
||||
|
||||
logger.debug("Cleaned up old jobs", {
|
||||
count: results.length,
|
||||
expirationDate,
|
||||
payload,
|
||||
});
|
||||
|
||||
if (this.#reporter) {
|
||||
await this.#reporter("cleanup_stats", {
|
||||
count: results.length,
|
||||
expirationDate,
|
||||
ts: payload._cron.ts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #handleReporter(rawPayload: unknown, helpers: JobHelpers): Promise<void> {
|
||||
if (!this.#reporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Received reporter task", {
|
||||
payload: rawPayload,
|
||||
});
|
||||
|
||||
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error(
|
||||
`Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}`
|
||||
);
|
||||
}
|
||||
|
||||
const payload = parsedPayload.data;
|
||||
|
||||
// Subtract an hour from the payload._cron.ts
|
||||
const startAt = new Date(payload._cron.ts.getTime() - 1000 * 60 * 60);
|
||||
|
||||
const schema = z.array(z.object({ count: z.coerce.number() }));
|
||||
|
||||
// Count the number of jobs that have been added since the startAt date and before the payload._cron.ts date
|
||||
const rawAddedResults = await this.#prisma.$queryRawUnsafe(
|
||||
`SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs WHERE created_at > $1 AND created_at < $2`,
|
||||
startAt,
|
||||
payload._cron.ts
|
||||
);
|
||||
|
||||
const addedCountResults = schema.parse(rawAddedResults)[0];
|
||||
|
||||
// Count the total number of jobs in the jobs table
|
||||
const rawTotalResults = await this.#prisma.$queryRawUnsafe(
|
||||
`SELECT COUNT(*) FROM ${this.graphileWorkerSchema}.jobs`
|
||||
);
|
||||
|
||||
const totalCountResults = schema.parse(rawTotalResults)[0];
|
||||
|
||||
logger.debug("Calculated metrics about the jobs table", {
|
||||
rawAddedResults,
|
||||
rawTotalResults,
|
||||
payload,
|
||||
});
|
||||
|
||||
await this.#reporter("queue_metrics", {
|
||||
addedCount: addedCountResults.count,
|
||||
totalCount: totalCountResults.count,
|
||||
ts: payload._cron.ts,
|
||||
});
|
||||
}
|
||||
|
||||
#logDebug(message: string, args?: any) {
|
||||
logger.debug(`[worker][${this.#name}] ${message}`, args);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { IndexEndpointStats, parseEndpointIndexStats } from "~/models/indexEndpoint.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import type {
|
||||
Endpoint,
|
||||
EndpointIndex,
|
||||
EndpointIndexStatus,
|
||||
RuntimeEnvironment,
|
||||
RuntimeEnvironmentType,
|
||||
} from "@trigger.dev/database";
|
||||
import {
|
||||
EndpointIndexError,
|
||||
EndpointIndexErrorSchema,
|
||||
IndexEndpointStats,
|
||||
parseEndpointIndexStats,
|
||||
} from "@trigger.dev/core";
|
||||
|
||||
export type Client = {
|
||||
slug: string;
|
||||
@@ -34,9 +40,11 @@ export type ClientEndpoint =
|
||||
url: string;
|
||||
indexWebhookPath: string;
|
||||
latestIndex?: {
|
||||
status: EndpointIndexStatus;
|
||||
source: string;
|
||||
updatedAt: Date;
|
||||
stats: IndexEndpointStats;
|
||||
stats?: IndexEndpointStats;
|
||||
error?: EndpointIndexError;
|
||||
};
|
||||
environment: {
|
||||
id: string;
|
||||
@@ -81,9 +89,11 @@ export class EnvironmentsPresenter {
|
||||
indexingHookIdentifier: true,
|
||||
indexings: {
|
||||
select: {
|
||||
status: true,
|
||||
source: true,
|
||||
updatedAt: true,
|
||||
stats: true,
|
||||
error: true,
|
||||
},
|
||||
take: 1,
|
||||
orderBy: {
|
||||
@@ -214,7 +224,7 @@ const environmentSortOrder: RuntimeEnvironmentType[] = [
|
||||
|
||||
function endpointClient(
|
||||
endpoint: Pick<Endpoint, "id" | "slug" | "url" | "indexingHookIdentifier"> & {
|
||||
indexings: Pick<EndpointIndex, "source" | "updatedAt" | "stats">[];
|
||||
indexings: Pick<EndpointIndex, "status" | "source" | "updatedAt" | "stats" | "error">[];
|
||||
},
|
||||
environment: Pick<RuntimeEnvironment, "id" | "apiKey" | "type">,
|
||||
baseUrl: string
|
||||
@@ -227,9 +237,13 @@ function endpointClient(
|
||||
indexWebhookPath: `${baseUrl}/api/v1/endpoints/${environment.id}/${endpoint.slug}/index/${endpoint.indexingHookIdentifier}`,
|
||||
latestIndex: endpoint.indexings[0]
|
||||
? {
|
||||
status: endpoint.indexings[0].status,
|
||||
source: endpoint.indexings[0].source,
|
||||
updatedAt: endpoint.indexings[0].updatedAt,
|
||||
stats: parseEndpointIndexStats(endpoint.indexings[0].stats),
|
||||
error: endpoint.indexings[0].error
|
||||
? EndpointIndexErrorSchema.parse(endpoint.indexings[0].error)
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
environment: environment,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
export class OrgUsagePresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -33,6 +34,7 @@ export class OrgUsagePresenter {
|
||||
createdAt: {
|
||||
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
},
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,6 +46,7 @@ export class OrgUsagePresenter {
|
||||
gte: startOfLastMonth,
|
||||
lt: startOfMonth,
|
||||
},
|
||||
internal: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,7 +66,7 @@ export class OrgUsagePresenter {
|
||||
month: string;
|
||||
count: number;
|
||||
}[]
|
||||
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' GROUP BY month ORDER BY month ASC`;
|
||||
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' AND "internal" = FALSE GROUP BY month ORDER BY month ASC`;
|
||||
|
||||
const chartData = chartDataRaw.map((obj) => ({
|
||||
name: obj.month,
|
||||
@@ -139,11 +142,13 @@ export class OrgUsagePresenter {
|
||||
},
|
||||
});
|
||||
|
||||
const chartDataDisplay = fillInMissingMonthlyData(chartData, 6);
|
||||
|
||||
return {
|
||||
id: organization.id,
|
||||
runsCount,
|
||||
runsCountLastMonth,
|
||||
chartData: fillInMissingMonthlyData(chartData, 6),
|
||||
chartData: chartDataDisplay,
|
||||
totalJobs,
|
||||
totalJobsLastMonth,
|
||||
totalIntegrations,
|
||||
@@ -166,7 +171,7 @@ function fillInMissingMonthlyData(
|
||||
|
||||
const startMonth = new Date(
|
||||
new Date(currentMonth).getFullYear(),
|
||||
new Date(currentMonth).getMonth() - totalNumberOfMonths,
|
||||
new Date(currentMonth).getMonth() - (totalNumberOfMonths - 2),
|
||||
1
|
||||
)
|
||||
.toISOString()
|
||||
@@ -182,17 +187,36 @@ function fillInMissingMonthlyData(
|
||||
return completeData;
|
||||
}
|
||||
|
||||
// Start month will be like 2023-03 and endMonth will be like 2023-10
|
||||
// The result should be an array of months between these two months, including the start and end month
|
||||
// So for example, if startMonth is 2023-03 and endMonth is 2023-10, the result should be:
|
||||
// ["2023-03", "2023-04", "2023-05", "2023-06", "2023-07", "2023-08", "2023-09", "2023-10"]
|
||||
function getMonthsBetween(startMonth: string, endMonth: string): string[] {
|
||||
const startDate = new Date(startMonth);
|
||||
const endDate = new Date(endMonth);
|
||||
// Initialize result array
|
||||
const result: string[] = [];
|
||||
|
||||
const months = [];
|
||||
let currentDate = startDate;
|
||||
// Parse the year and month from startMonth and endMonth
|
||||
let [startYear, startMonthNum] = startMonth.split("-").map(Number);
|
||||
let [endYear, endMonthNum] = endMonth.split("-").map(Number);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
months.push(currentDate.toISOString().slice(0, 7));
|
||||
currentDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1));
|
||||
// Loop through each month between startMonth and endMonth
|
||||
for (let year = startYear; year <= endYear; year++) {
|
||||
let monthStart = year === startYear ? startMonthNum : 1;
|
||||
let monthEnd = year === endYear ? endMonthNum : 12;
|
||||
|
||||
for (let month = monthStart; month <= monthEnd; month++) {
|
||||
// Format the month into a string and add it to the result array
|
||||
result.push(`${year}-${String(month).padStart(2, "0")}`);
|
||||
}
|
||||
}
|
||||
|
||||
return months;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getLastSecondOfMonth(endMonth: string) {
|
||||
const [year, month] = endMonth.split("-").map(Number);
|
||||
const nextMonthFirstDay = new Date(year, month, 1);
|
||||
nextMonthFirstDay.setDate(0);
|
||||
nextMonthFirstDay.setHours(23, 59, 59);
|
||||
return nextMonthFirstDay;
|
||||
}
|
||||
|
||||
@@ -100,21 +100,24 @@ export class RunListPresenter {
|
||||
let previous: string | undefined;
|
||||
switch (direction) {
|
||||
case "forward":
|
||||
previous = cursor ? runs.at(0)?.id : undefined;
|
||||
if (hasMore) {
|
||||
next = runs[PAGE_SIZE - 1]?.id;
|
||||
}
|
||||
previous = cursor ? runs.at(1)?.id : undefined;
|
||||
break;
|
||||
case "backward":
|
||||
if (hasMore) {
|
||||
next = runs[PAGE_SIZE - 1]?.id;
|
||||
previous = runs[1]?.id;
|
||||
}
|
||||
previous = runs.at(1)?.id;
|
||||
next = runs[PAGE_SIZE - 1]?.id;
|
||||
break;
|
||||
}
|
||||
|
||||
const runsToReturn =
|
||||
direction === "backward" && hasMore ? runs.slice(1, PAGE_SIZE + 1) : runs.slice(0, PAGE_SIZE);
|
||||
|
||||
return {
|
||||
runs: runs.slice(0, PAGE_SIZE).map((run) => ({
|
||||
runs: runsToReturn.map((run) => ({
|
||||
id: run.id,
|
||||
number: run.number,
|
||||
startedAt: run.startedAt,
|
||||
|
||||
+13
-10
@@ -1,7 +1,10 @@
|
||||
import type { LinksFunction, LoaderArgs } from "@remix-run/node";
|
||||
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
|
||||
import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
|
||||
import { metaV1 } from "@remix-run/v1-meta";
|
||||
import { TypedMetaFunction, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExternalScripts } from "remix-utils/external-scripts";
|
||||
import type { ToastMessage } from "~/models/message.server";
|
||||
import { commitSession, getSession } from "~/models/message.server";
|
||||
import tailwindStylesheetUrl from "~/tailwind.css";
|
||||
@@ -11,24 +14,24 @@ import { AppContainer, MainCenteredContainer } from "./components/layout/AppLayo
|
||||
import { Toast } from "./components/primitives/Toast";
|
||||
import { env } from "./env.server";
|
||||
import { featuresForRequest } from "./features.server";
|
||||
import { useHighlight } from "./hooks/useHighlight";
|
||||
import { usePostHog } from "./hooks/usePostHog";
|
||||
import { getUser } from "./services/session.server";
|
||||
import { appEnvTitleTag } from "./utils";
|
||||
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
|
||||
import { useHighlight } from "./hooks/useHighlight";
|
||||
import { ExternalScripts } from "remix-utils";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
|
||||
};
|
||||
|
||||
export const meta: TypedMetaFunction<typeof loader> = ({ data }) => ({
|
||||
title: `Trigger.dev${appEnvTitleTag(data?.appEnv)}`,
|
||||
charset: "utf-8",
|
||||
viewport: "width=device-width,initial-scale=1",
|
||||
});
|
||||
export const meta: TypedMetaFunction<typeof loader> = (args) => {
|
||||
return metaV1(args, {
|
||||
title: `Trigger.dev${appEnvTitleTag(args.data.appEnv)}`,
|
||||
charset: "utf-8",
|
||||
viewport: "width=1024, initial-scale=1",
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
const toastMessage = session.get("toastMessage") as ToastMessage;
|
||||
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { useOptionalOrganizations } from "~/hooks/useOrganizations";
|
||||
@@ -11,7 +11,7 @@ import { Link } from "@remix-run/react";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
//if there are invites then we should redirect to the invites page
|
||||
|
||||
@@ -113,7 +113,8 @@ export default function Page() {
|
||||
|
||||
const [form, { name, email, marketingEmails }] = useForm({
|
||||
id: "account",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: createSchema() });
|
||||
},
|
||||
|
||||
@@ -14,12 +14,12 @@ import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder";
|
||||
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
|
||||
import { Link } from "@remix-run/react/dist/components";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ params, request }: LoaderArgs) {
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug } = OrganizationParamsSchema.parse(params);
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ export default function Page() {
|
||||
|
||||
const [form, { emails }] = useForm({
|
||||
id: "invite-members",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
|
||||
+18
-6
@@ -1,5 +1,5 @@
|
||||
import { ArrowUpIcon } from "@heroicons/react/24/solid";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { FrameworkSelector } from "~/components/frameworks/FrameworkSelector";
|
||||
import { JobsTable } from "~/components/jobs/JobsTable";
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Switch } from "~/components/primitives/Switch";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { useFilterJobs } from "~/hooks/useFilterJobs";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
trimTrailingSlash,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
@@ -62,8 +63,11 @@ export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const { jobs } = useTypedLoaderData<typeof loader>();
|
||||
const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs);
|
||||
const hasJobs = jobs.length > 0;
|
||||
const { filterText, setFilterText, filteredItems, onlyActiveJobs, setOnlyActiveJobs } =
|
||||
useFilterJobs(jobs);
|
||||
const totalJobs = jobs.length;
|
||||
const hasJobs = totalJobs > 0;
|
||||
const activeJobCount = jobs.filter((j) => j.status === "ACTIVE").length;
|
||||
|
||||
return (
|
||||
<PageContainer className={hasJobs ? "" : "grid-rows-1"}>
|
||||
@@ -74,7 +78,8 @@ export default function Page() {
|
||||
</PageTitleRow>
|
||||
<PageInfoRow>
|
||||
<PageInfoGroup>
|
||||
<PageInfoProperty icon={"job"} label={"Active Jobs"} value={jobs.length} />
|
||||
<PageInfoProperty icon={"job"} label={"All Jobs"} value={totalJobs} />
|
||||
<PageInfoProperty icon={"job"} label={"Active Jobs"} value={activeJobCount} />
|
||||
</PageInfoGroup>
|
||||
</PageInfoRow>
|
||||
</PageHeader>
|
||||
@@ -96,7 +101,7 @@ export default function Page() {
|
||||
</Callout>
|
||||
)}
|
||||
<div className="mb-2 flex flex-col">
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-full gap-x-2">
|
||||
<Input
|
||||
placeholder="Search Jobs"
|
||||
variant="tertiary"
|
||||
@@ -106,6 +111,13 @@ export default function Page() {
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Active Jobs"
|
||||
checked={onlyActiveJobs}
|
||||
onCheckedChange={setOnlyActiveJobs}
|
||||
className={"shrink-0"}
|
||||
/>
|
||||
<HelpTrigger title="Example Jobs and inspiration" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { EnvironmentsStreamPresenter } from "~/presenters/EnvironmentsStreamPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { ProjectParamSchema } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
|
||||
+49
-13
@@ -2,11 +2,15 @@ import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { useEffect } from "react";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { useEventSource } from "remix-utils/sse/react";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import {
|
||||
EndpointIndexStatusIcon,
|
||||
EndpointIndexStatusLabel,
|
||||
} from "~/components/environments/EndpointIndexStatus";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Callout, CalloutVariant } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
@@ -18,7 +22,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Sheet, SheetBody, SheetContent, SheetHeader } from "~/components/primitives/Sheet";
|
||||
import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server";
|
||||
import { endpointStreamingPath } from "~/utils/pathBuilder";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { EndpointIndexStatus, RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
|
||||
|
||||
type ConfigureEndpointSheetProps = {
|
||||
@@ -33,7 +37,7 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
|
||||
const [form, { url, clientSlug }] = useForm({
|
||||
id: "endpoint-url",
|
||||
lastSubmission: setEndpointUrlFetcher.data,
|
||||
lastSubmission: setEndpointUrlFetcher.data as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: bodySchema });
|
||||
},
|
||||
@@ -119,15 +123,29 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
method="post"
|
||||
action={`/resources/environments/${endpoint.environment.id}/endpoint/${endpoint.id}`}
|
||||
>
|
||||
<Callout variant="success" className="justiy-between items-center">
|
||||
<Paragraph variant="small" className="grow text-green-200">
|
||||
Endpoint configured. Last refreshed:{" "}
|
||||
{endpoint.latestIndex ? (
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
<Callout
|
||||
variant="info"
|
||||
icon={
|
||||
<EndpointIndexStatusIcon status={endpoint.latestIndex?.status ?? "PENDING"} />
|
||||
}
|
||||
className="justiy-between items-center"
|
||||
>
|
||||
<div className="flex grow items-center gap-2">
|
||||
<EndpointIndexStatusLabel
|
||||
status={endpoint.latestIndex?.status ?? "PENDING"}
|
||||
/>
|
||||
<Paragraph variant="small" className="grow">
|
||||
Last refreshed:{" "}
|
||||
{endpoint.latestIndex ? (
|
||||
<>
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
@@ -138,6 +156,11 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
{refreshingEndpoint ? "Refreshing" : "Refresh now"}
|
||||
</Button>
|
||||
</Callout>
|
||||
{endpoint.latestIndex?.error && (
|
||||
<FormError className="p-2">
|
||||
<pre>{endpoint.latestIndex.error.message}</pre>
|
||||
</FormError>
|
||||
)}
|
||||
</refreshEndpointFetcher.Form>
|
||||
</div>
|
||||
<div className="max-w-full overflow-hidden">
|
||||
@@ -155,3 +178,16 @@ export function ConfigureEndpointSheet({ slug, endpoint, onClose }: ConfigureEnd
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function calloutVariantFromStatus(status: EndpointIndexStatus): CalloutVariant {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "pending";
|
||||
case "STARTED":
|
||||
return "pending";
|
||||
case "SUCCESS":
|
||||
return "success";
|
||||
case "FAILURE":
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ export function FirstEndpointSheet({ projectId, environments }: FirstEndpointShe
|
||||
const setEndpointUrlFetcher = useFetcher();
|
||||
const [form, { url, environmentId }] = useForm({
|
||||
id: "new-endpoint-url",
|
||||
lastSubmission: setEndpointUrlFetcher.data,
|
||||
// TODO: type this
|
||||
lastSubmission: setEndpointUrlFetcher.data as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: bodySchema });
|
||||
},
|
||||
|
||||
+34
-12
@@ -1,13 +1,18 @@
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { useEventSource } from "remix-utils/sse/react";
|
||||
import {
|
||||
EndpointIndexStatusIcon,
|
||||
EndpointIndexStatusLabel,
|
||||
} from "~/components/environments/EndpointIndexStatus";
|
||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
@@ -38,10 +43,10 @@ import { ProjectParamSchema, projectEnvironmentsStreamingPath } from "~/utils/pa
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { FirstEndpointSheet } from "./FirstEndpointSheet";
|
||||
import { RegenerateApiKeyModal } from "~/components/environments/RegenerateApiKeyModal";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
@@ -144,11 +149,17 @@ export default function Page() {
|
||||
</Paragraph>
|
||||
<div className="mt-4 flex flex-col gap-6">
|
||||
{environments.map((environment) => (
|
||||
<div key={environment.id}>
|
||||
<Header3 className="flex items-center gap-1">
|
||||
<EnvironmentLabel environment={environment} /> Environment
|
||||
</Header3>
|
||||
<div className="mt-2 inline-flex flex-col gap-3">
|
||||
<div key={environment.id} className="w-[400px]">
|
||||
<div className="flex justify-between">
|
||||
<Header3 className="flex items-center gap-1">
|
||||
<EnvironmentLabel environment={environment} /> Environment
|
||||
</Header3>
|
||||
<RegenerateApiKeyModal
|
||||
id={environment.id}
|
||||
title={environmentTitle(environment)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 inline-flex w-full flex-col gap-3">
|
||||
<ClipboardField
|
||||
className="w-full max-w-none"
|
||||
secure
|
||||
@@ -180,6 +191,7 @@ export default function Page() {
|
||||
<TableHeaderCell>Environment</TableHeaderCell>
|
||||
<TableHeaderCell>Url</TableHeaderCell>
|
||||
<TableHeaderCell>Last refreshed</TableHeaderCell>
|
||||
<TableHeaderCell>Last refresh Status</TableHeaderCell>
|
||||
<TableHeaderCell>Jobs</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -268,7 +280,7 @@ function EndpointRow({
|
||||
<EnvironmentLabel environment={{ type }} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick} colSpan={4} alignment="right">
|
||||
<TableCell onClick={onClick} colSpan={5} alignment="right">
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<span className="text-amber-500">
|
||||
The {environmentTitle({ type })} environment is not configured
|
||||
@@ -290,7 +302,17 @@ function EndpointRow({
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex ? <DateTime date={endpoint.latestIndex.updatedAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats.jobs ?? "–"}</TableCell>
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<EndpointIndexStatusIcon status={endpoint.latestIndex.status} />
|
||||
<EndpointIndexStatusLabel status={endpoint.latestIndex.status} />
|
||||
</div>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.latestIndex?.stats?.jobs ?? "–"}</TableCell>
|
||||
<TableCellChevron onClick={onClick} />
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useCallback, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
integrationClientPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
@@ -152,7 +152,7 @@ function PossibleIntegrationsList({
|
||||
onCheckedChange={setOnlyShowIntegrations}
|
||||
variant="small"
|
||||
label={
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="mt-0.5 inline-flex items-center gap-1">
|
||||
<IntegrationIcon /> Trigger.dev Integrations
|
||||
</span>
|
||||
}
|
||||
|
||||
+8
-9
@@ -1,4 +1,4 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { HowToUseThisIntegration } from "~/components/helpContent/HelpContentText";
|
||||
import { JobSkeleton } from "~/components/jobs/JobSkeleton";
|
||||
@@ -8,6 +8,7 @@ import { Callout } from "~/components/primitives/Callout";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { useFilterJobs } from "~/hooks/useFilterJobs";
|
||||
import { useIntegrationClient } from "~/hooks/useIntegrationClient";
|
||||
import { JobListPresenter } from "~/presenters/JobListPresenter.server";
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
trimTrailingSlash,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam, clientParam } =
|
||||
IntegrationClientParamSchema.parse(params);
|
||||
@@ -52,10 +53,8 @@ export default function Page() {
|
||||
{(open) => (
|
||||
<div className={cn("grid h-full gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
|
||||
<div className="grow">
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
{jobs.length === 0 ? (
|
||||
<Header2>Jobs using this integration will appear here</Header2>
|
||||
) : (
|
||||
<div className="mb-2 flex items-center justify-end gap-x-2">
|
||||
{jobs.length !== 0 && (
|
||||
<Input
|
||||
placeholder="Search Jobs"
|
||||
variant="tertiary"
|
||||
@@ -68,9 +67,9 @@ export default function Page() {
|
||||
<HelpTrigger title="How do I use this integration?" />
|
||||
</div>
|
||||
{jobs.length === 0 ? (
|
||||
<>
|
||||
<JobSkeleton />
|
||||
</>
|
||||
<div className="mt-8 rounded border border-border px-2 py-6 text-center">
|
||||
<Paragraph variant="small">Jobs using this Integration will appear here.</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
<JobsTable
|
||||
jobs={filteredItems}
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { connectionType } from "~/components/integrations/connectionType";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
@@ -18,7 +18,7 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { IntegrationClientParamSchema, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam, clientParam } =
|
||||
IntegrationClientParamSchema.parse(params);
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
@@ -7,7 +7,7 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { IntegrationClientParamSchema, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam, clientParam } =
|
||||
IntegrationClientParamSchema.parse(params);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { connectionType } from "~/components/integrations/connectionType";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
projectIntegrationsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const { organizationSlug, clientParam, projectParam } =
|
||||
IntegrationClientParamSchema.parse(params);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { HowToRunYourJob } from "~/components/helpContent/HelpContentText";
|
||||
@@ -30,7 +30,7 @@ export const RunListSearchSchema = z.object({
|
||||
direction: DirectionSchema.optional(),
|
||||
});
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { jobParam, projectParam, organizationSlug } = JobParamsSchema.parse(params);
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { RunStreamPresenter } from "~/presenters/RunStreamPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderArgs) {
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
|
||||
const { runParam } = z.object({ runParam: z.string() }).parse(params);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Await, useLoaderData } from "@remix-run/react";
|
||||
import { LoaderArgs, SerializeFrom, defer } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs, SerializeFrom, defer } from "@remix-run/server-runtime";
|
||||
import { Suspense } from "react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { TaskDetail } from "~/components/run/TaskDetail";
|
||||
@@ -7,7 +7,7 @@ import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { TaskParamsSchema } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { taskParam } = TaskParamsSchema.parse(params);
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { TriggerDetail } from "~/components/run/TriggerDetail";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
@@ -6,7 +6,7 @@ import { useRun } from "~/hooks/useRun";
|
||||
import { TriggerDetailsPresenter } from "~/presenters/TriggerDetailsPresenter.server";
|
||||
import { RunParamsSchema } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { runParam } = RunParamsSchema.parse(params);
|
||||
|
||||
const presenter = new TriggerDetailsPresenter();
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment, useEffect } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { useEventSource } from "remix-utils/sse/react";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
|
||||
import { RunOverview } from "~/components/run/RunOverview";
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
trimTrailingSlash,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { runParam } = RunParamsSchema.parse(params);
|
||||
|
||||
|
||||
+6
-4
@@ -3,7 +3,7 @@ import { parse } from "@conform-to/zod";
|
||||
import { ClipboardIcon } from "@heroicons/react/20/solid";
|
||||
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
|
||||
import { Form, useActionData, useSubmit } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
@@ -35,9 +35,10 @@ import { TestJobService } from "~/services/jobs/testJob.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { isValidIcon } from "~/utils/icon";
|
||||
import { JobParamsSchema, jobRunDashboardPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam, jobParam } = JobParamsSchema.parse(params);
|
||||
|
||||
@@ -165,7 +166,8 @@ export default function Page() {
|
||||
|
||||
const [form, { environmentId, payload, accountId }] = useForm({
|
||||
id: "test-job",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
@@ -228,7 +230,7 @@ export default function Page() {
|
||||
}}
|
||||
>
|
||||
<DetailCell
|
||||
leadingIcon={example.icon ?? CodeBracketIcon}
|
||||
leadingIcon={isValidIcon(example.icon) ? example.icon : CodeBracketIcon}
|
||||
leadingIconClassName="text-blue-500"
|
||||
label={example.name}
|
||||
trailingIcon={example.id === selectedCodeSampleId ? "check" : "plus"}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Outlet, useLocation } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment } from "react";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import { JobStatusBadge } from "~/components/jobs/JobStatusBadge";
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
trimTrailingSlash,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { jobParam, projectParam, organizationSlug } = JobParamsSchema.parse(params);
|
||||
|
||||
|
||||
+102
-12
@@ -1,23 +1,113 @@
|
||||
import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo";
|
||||
import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon";
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { PageGradient } from "~/components/PageGradient";
|
||||
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => (
|
||||
<BreadcrumbLink to={trimTrailingSlash(match.pathname)} title="SvelteKit" />
|
||||
),
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
export default function SetUpSveltekit() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
return (
|
||||
<FrameworkComingSoon
|
||||
frameworkName="SvelteKit"
|
||||
githubIssueUrl="https://github.com/triggerdotdev/trigger.dev/issues/453"
|
||||
githubIssueNumber={453}
|
||||
>
|
||||
<SvelteKitLogo className="w-56" />
|
||||
</FrameworkComingSoon>
|
||||
<PageGradient>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<Header1 spacing className="text-bright">
|
||||
Get setup in 5 minutes
|
||||
</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkButton
|
||||
to={projectSetupPath(organization, project)}
|
||||
variant="tertiary/small"
|
||||
LeadingIcon={Squares2X2Icon}
|
||||
>
|
||||
Choose a different framework
|
||||
</LinkButton>
|
||||
<Feedback
|
||||
button={
|
||||
<Button variant="tertiary/small" LeadingIcon={ChatBubbleLeftRightIcon}>
|
||||
I'm stuck!
|
||||
</Button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Callout
|
||||
variant={"info"}
|
||||
to="https://github.com/triggerdotdev/trigger.dev/discussions/430"
|
||||
className="mb-8"
|
||||
>
|
||||
Trigger.dev has full support for serverless. We will be adding support for long-running
|
||||
servers soon.
|
||||
</Callout>
|
||||
<div>
|
||||
<StepNumber
|
||||
stepNumber="1"
|
||||
title="Follow the steps from the Sveltekit manual installation guide"
|
||||
/>
|
||||
<StepContentContainer className="flex flex-col gap-2">
|
||||
<Paragraph className="mt-2">Copy your server API Key to your clipboard:</Paragraph>
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>Now follow this guide:</Paragraph>
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/manual/sveltekit"
|
||||
variant="primary/medium"
|
||||
TrailingIcon="external-link"
|
||||
>
|
||||
Manual installation guide
|
||||
</LinkButton>
|
||||
<div className="flex items-start justify-start gap-2"></div>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run your sveltekit app" />
|
||||
<StepContentContainer>
|
||||
<RunDevCommand extra=" -- --open --host" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStep extra=" --port 5173" />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="6" title="Wait for Jobs" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGradient>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
@@ -25,7 +25,7 @@ import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { ProjectParamSchema, externalTriggerPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function Integrations() {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={5}>
|
||||
<TableBlankRow colSpan={100}>
|
||||
<Paragraph>No External triggers</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { NoSymbolIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
@@ -25,7 +25,7 @@ import { requireUser } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { ProjectParamSchema, docsPath, trimTrailingSlash } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function Integrations() {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBlankRow colSpan={5}>
|
||||
<TableBlankRow colSpan={100}>
|
||||
<Paragraph>No Scheduled triggers</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
import { Response, json } from "@remix-run/node";
|
||||
import type { ActionFunction, LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/node";
|
||||
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
@@ -45,7 +45,7 @@ import { ActivateSourceService } from "~/services/sources/activateSource.server"
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const { organizationSlug, projectParam, triggerParam } = TriggerSourceParamSchema.parse(params);
|
||||
|
||||
@@ -135,7 +135,8 @@ export default function Page() {
|
||||
|
||||
const [form, { jobId }] = useForm({
|
||||
id: "trigger-registration-retry",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { RunStreamPresenter } from "~/presenters/RunStreamPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderArgs) {
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
|
||||
const { runParam } = z.object({ runParam: z.string() }).parse(params);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Await, useLoaderData } from "@remix-run/react";
|
||||
import { LoaderArgs, defer } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs, defer } from "@remix-run/server-runtime";
|
||||
import { Suspense } from "react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { TaskDetail } from "~/components/run/TaskDetail";
|
||||
@@ -7,7 +7,7 @@ import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { TriggerSourceRunTaskParamsSchema } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { taskParam } = TriggerSourceRunTaskParamsSchema.parse(params);
|
||||
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { TriggerDetail } from "~/components/run/TriggerDetail";
|
||||
import { TriggerDetailsPresenter } from "~/presenters/TriggerDetailsPresenter.server";
|
||||
import { TriggerSourceRunParamsSchema } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { runParam } = TriggerSourceRunParamsSchema.parse(params);
|
||||
|
||||
const presenter = new TriggerDetailsPresenter();
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment, useEffect } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { useEventSource } from "remix-utils/sse/react";
|
||||
import { BreadcrumbLink } from "~/components/navigation/NavBar";
|
||||
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
|
||||
import { RunOverview } from "~/components/run/RunOverview";
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
trimTrailingSlash,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { runParam, triggerParam } = TriggerSourceRunParamsSchema.parse(params);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
|
||||
@@ -13,7 +13,7 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam } = params;
|
||||
invariant(projectParam, "projectParam not found");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import type { ActionFunction } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { json } from "@remix-run/node";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
@@ -11,7 +11,6 @@ import { Fieldset } from "~/components/primitives/Fieldset";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { FormTitle } from "~/components/primitives/FormTitle";
|
||||
import { Hint } from "~/components/primitives/Hint";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
@@ -60,7 +59,8 @@ export default function NewOrganizationPage() {
|
||||
|
||||
const [form, { projectName }] = useForm({
|
||||
id: "create-project",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { UserPlusIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { useState } from "react";
|
||||
import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
@@ -33,7 +33,7 @@ import { inviteTeamMemberPath, organizationTeamPath, resendInvitePath } from "~/
|
||||
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug } = params;
|
||||
invariant(organizationSlug, "organizationSlug not found");
|
||||
@@ -235,7 +235,8 @@ function LeaveTeamModal({
|
||||
|
||||
const [form, { memberId }] = useForm({
|
||||
id: "remove-member",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
|
||||
@@ -11,7 +11,7 @@ import { commitCurrentOrgSession, setCurrentOrg } from "~/services/currentOrgani
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { organizationPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug } = params;
|
||||
invariant(organizationSlug, "organizationSlug not found");
|
||||
|
||||
@@ -56,7 +56,8 @@ export default function NewOrganizationPage() {
|
||||
|
||||
const [form, { orgName, projectName }] = useForm({
|
||||
id: "create-organization",
|
||||
lastSubmission,
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema });
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Outlet, ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
|
||||
import { ImpersonationBanner } from "~/components/ImpersonationBanner";
|
||||
@@ -13,7 +13,7 @@ import { clearRedirectTo, commitSession } from "~/services/redirectTo.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { confirmBasicDetailsPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const organizations = await getOrganizations({ userId: user.id });
|
||||
const impersonationId = await getImpersonationId(request);
|
||||
@@ -60,7 +60,6 @@ export default function App() {
|
||||
return (
|
||||
<>
|
||||
{impersonationId && <ImpersonationBanner impersonationId={impersonationId} />}
|
||||
<NoMobileOverlay />
|
||||
<AppContainer showBackgroundGradient={showBackgroundGradient}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
@@ -72,7 +71,6 @@ export default function App() {
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<>
|
||||
<NoMobileOverlay />
|
||||
<AppContainer showBackgroundGradient={true}>
|
||||
<MainCenteredContainer>
|
||||
<RouteErrorDisplay />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Form } from "@remix-run/react";
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
@@ -15,7 +15,7 @@ export async function loader() {
|
||||
|
||||
const FormSchema = z.object({ id: z.string() });
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
if (request.method.toLowerCase() !== "post") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
}
|
||||
@@ -30,89 +30,80 @@ export async function action({ request }: ActionArgs) {
|
||||
});
|
||||
}
|
||||
|
||||
const headerClassName =
|
||||
"py-3 px-2 pr-3 text-xs font-semibold leading-tight text-slate-900 text-left";
|
||||
const cellClassName = "whitespace-nowrap px-2 py-2 text-xs text-slate-500";
|
||||
const headerClassName = "py-3 px-2 pr-3 text-xs font-semibold leading-tight text-bright text-left";
|
||||
const cellClassName = "whitespace-nowrap px-2 py-2 text-xs text-bright";
|
||||
|
||||
export default function AdminDashboardRoute() {
|
||||
const { users } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 overflow-hidden">
|
||||
{/* Primary column */}
|
||||
<section
|
||||
aria-labelledby="primary-heading"
|
||||
className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto p-4 lg:order-last"
|
||||
>
|
||||
<h1 className="mb-2 text-2xl">Accounts ({users.length})</h1>
|
||||
<main
|
||||
aria-labelledby="primary-heading"
|
||||
className="flex h-full min-w-0 flex-1 flex-col overflow-y-auto p-4 lg:order-last"
|
||||
>
|
||||
<h1 className="mb-2 text-2xl">Accounts ({users.length})</h1>
|
||||
|
||||
<table className="w-full divide-y divide-slate-300">
|
||||
<thead className="sticky top-0 bg-white text-left outline outline-2 outline-slate-200">
|
||||
<tr>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Email
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
GitHub username
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
id
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Created At
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Admin?
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200 bg-white">
|
||||
{users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id} className="w-full bg-white px-4 py-2 text-left hover:bg-slate-50">
|
||||
<td className={cellClassName}>{user.email}</td>
|
||||
<td className={cellClassName}>
|
||||
<a
|
||||
href={`https://github.com/${user.displayName}`}
|
||||
target="_blank"
|
||||
className="text-indigo-500 underline"
|
||||
rel="noreferrer"
|
||||
<table className="w-full divide-y divide-border">
|
||||
<thead className="sticky -top-4 bg-midnight-800 text-left">
|
||||
<tr>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Email
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
GitHub username
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
id
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Created At
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Admin?
|
||||
</th>
|
||||
<th scope="col" className={headerClassName}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id} className="w-full px-4 py-2 text-left hover:bg-slate-900">
|
||||
<td className={cellClassName}>{user.email}</td>
|
||||
<td className={cellClassName}>
|
||||
<a
|
||||
href={`https://github.com/${user.displayName}`}
|
||||
target="_blank"
|
||||
className="text-indigo-500 underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{user.displayName}
|
||||
</a>
|
||||
</td>
|
||||
<td className={cellClassName}>{user.id}</td>
|
||||
<td className={cellClassName}>{user.createdAt.toISOString()}</td>
|
||||
<td className={cellClassName}>{user.admin ? "✅" : ""}</td>
|
||||
<td className={cellClassName}>
|
||||
<Form method="post" reloadDocument>
|
||||
<input type="hidden" name="id" value={user.id} />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
name="action"
|
||||
value="impersonate"
|
||||
className="mr-2"
|
||||
variant="primary/small"
|
||||
>
|
||||
{user.displayName}
|
||||
</a>
|
||||
</td>
|
||||
<td className={cellClassName}>{user.id}</td>
|
||||
<td className={cellClassName}>{user.createdAt.toISOString()}</td>
|
||||
<td className={cellClassName}>{user.admin ? "✅" : ""}</td>
|
||||
<td className={cellClassName}>
|
||||
<Form method="post" reloadDocument>
|
||||
<input type="hidden" name="id" value={user.id} />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
name="action"
|
||||
value="impersonate"
|
||||
className="mr-2"
|
||||
variant="primary/small"
|
||||
>
|
||||
Impersonate
|
||||
</Button>
|
||||
</Form>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
{/* Secondary column (hidden on smaller screens) */}
|
||||
<aside className="hidden lg:order-first lg:block lg:flex-shrink-0">
|
||||
<div className="relative flex h-full w-96 flex-col overflow-y-auto border-r border-gray-200 bg-white"></div>
|
||||
</aside>
|
||||
Impersonate
|
||||
</Button>
|
||||
</Form>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ActionFunctionArgs, json, redirect } from "@remix-run/server-runtime";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const adminOrgMembers = await prisma.orgMember.findMany({
|
||||
where: {
|
||||
organizationId: authenticationResult.environment.organizationId,
|
||||
user: {
|
||||
admin: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!adminOrgMembers.length) {
|
||||
return json({ error: "You must be an admin to perform this action" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body: any = await request.json();
|
||||
|
||||
await workerQueue.enqueue("simulate", {
|
||||
seconds: body.seconds,
|
||||
});
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { HomeIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { UserCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { HomeIcon } from "@heroicons/react/24/outline";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment, useState } from "react";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import type { User } from "~/models/user.server";
|
||||
import { getUser, requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const navigation = [{ name: "Home", href: "/admin", icon: HomeIcon }];
|
||||
|
||||
export async function loader({ request }: LoaderArgs) {
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
const user = await getUser(request);
|
||||
if (user == null) {
|
||||
@@ -27,162 +20,10 @@ export async function loader({ request }: LoaderArgs) {
|
||||
|
||||
export default function Page() {
|
||||
const data = useTypedLoaderData<typeof loader>();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full">
|
||||
<Transition.Root show={mobileMenuOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-40 lg:hidden" onClose={setMobileMenuOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white focus:outline-none">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute top-0 right-0 -mr-12 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
<div className="pt-5 pb-4">
|
||||
<div className="flex flex-shrink-0 items-center px-4">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600"
|
||||
alt="Workflow"
|
||||
/>
|
||||
</div>
|
||||
<nav aria-label="Sidebar" className="mt-5">
|
||||
<div className="space-y-1 px-2">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="group flex items-center rounded-md p-2 text-base font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||
>
|
||||
<item.icon
|
||||
className="mr-4 h-6 w-6 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 border-t border-gray-200 p-4">
|
||||
<button className="group block flex-shrink-0">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<UserProfilePhoto user={data.user} className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-base font-medium text-gray-700 group-hover:text-gray-900">
|
||||
{data.user.displayName}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-500 group-hover:text-gray-700">
|
||||
Account Settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<div className="w-14 flex-shrink-0" aria-hidden="true">
|
||||
{/* Force sidebar to shrink to fit close icon */}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="flex w-20 flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-indigo-600">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-center bg-indigo-700 py-4">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://tailwindui.com/img/logos/workflow-mark.svg?color=white"
|
||||
alt="Workflow"
|
||||
/>
|
||||
</div>
|
||||
<nav aria-label="Sidebar" className="flex flex-col items-center space-y-3 py-6">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="flex items-center rounded-lg p-4 text-indigo-200 hover:bg-indigo-700"
|
||||
>
|
||||
<item.icon className="h-6 w-6" aria-hidden="true" />
|
||||
<span className="sr-only">{item.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 pb-5">
|
||||
<button className="flex w-full flex-shrink-0 flex-grow justify-center">
|
||||
<UserProfilePhoto user={data.user} className="block h-10 w-10" />
|
||||
<div className="sr-only">
|
||||
<p>{data.user.displayName}</p>
|
||||
<p>Account settings</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UserProfilePhoto({ user, className }: { user: User; className?: string }) {
|
||||
return user.avatarUrl ? (
|
||||
<img
|
||||
className={cn("rounded-full", className)}
|
||||
src={user.avatarUrl}
|
||||
alt={user.name ?? user.displayName ?? "User"}
|
||||
/>
|
||||
) : (
|
||||
<UserCircleIcon className={cn("text-gray-400", className)} />
|
||||
<div className="h-full w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
|
||||
/*
|
||||
To use this route, use the stripe CLI to forward events to this route:
|
||||
@@ -9,7 +9,7 @@ import { ActionArgs } from "@remix-run/server-runtime";
|
||||
|
||||
stripe trigger price.created
|
||||
*/
|
||||
export async function action({ request }: ActionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const body: any = await request.json();
|
||||
|
||||
const response = await fetch("https://jsonhero.io/api/create.json", {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user