Compare commits

...

82 Commits

Author SHA1 Message Date
Eric Allam 6881de84ab Fix pnpm lock file 2023-10-26 20:04:03 +01:00
github-actions[bot] 0ca8296b9a chore: Update version for release (#686)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-26 20:03:37 +01:00
Eric Allam 55946fcdb7 Update @trigger.dev/nextjs to support Next.js 14 (#690) 2023-10-26 19:45:19 +01:00
Eric Allam c0f9f10b07 Upgrade to Remix v2.1.0 2023-10-26 15:54:21 +01:00
Eric Allam e80aa2826d @trigger.dev/express: Remove web-fetch dependency and use native node Request/Response 2023-10-26 13:42:20 +01:00
NIXEY 1be25d7639 BlankRow centered (#678) 2023-10-26 13:35:15 +01:00
ROMIT PAL 3c3a5e1f28 added a missing CODE OF CONDUCT file (#677)
Incorporating a Code of Conduct in your GitHub repository is crucial for promoting an inclusive, respectful community. It establishes guidelines, minimizes conflicts, safeguards participants, and adheres to legal standards. Additionally, it demonstrates professionalism and aligns with GitHub's best practices, contributing to a welcoming and harmonious environment, which, in turn, enhances collaboration and the project's overall success.
2023-10-26 13:30:14 +01:00
Vineet J Karni 8e741c7218 Added warning ⚠️ - No support for long-running servers (#669) 2023-10-26 13:18:28 +01:00
Hemachandar 83455e3ad4 feat: implement API key regeneration (#649) 2023-10-26 13:15:50 +01:00
Ronit Panda f0f0420280 feat: adds helm charts (#636)
* feat: adds helm charts template and adds postgres deps

* feat: adds all depl (barebones)

* feat: adds all depl (barebones)

* fix: removes print

* fix: image pulling issue

* fix: templatisation complete

* style: cleanup

* feat: works

* feat: adds ingress and some docs

* fix: ingress bug --> routing to cluster works

* feat: completes docs v1 todo --> needs hosting of the helm chart on chosen platform
2023-10-26 11:50:59 +01:00
Hemachandar c4533c36cb fix: set error message in runTask & executeJob (#634)
* fix: set error message in runTask & executeJob

* add changeset
2023-10-26 11:46:09 +01:00
Eric Allam f8e034c190 Fixed import that was causing typecheck to fail 2023-10-26 11:43:23 +01:00
Eric Allam 21f62e780c Added tests and refactored response to use camelCase 2023-10-26 11:30:49 +01:00
hmacr c3564ca0d4 fix doc 2023-10-26 11:18:11 +01:00
hmacr 6e1b8a11d4 feat: allow cancelling job runs for an event-id 2023-10-26 11:18:11 +01:00
Matt Aitken 9900f19b70 Migration: Updates any Endpoint runChunkExecutionLimits that were 60ms to be 60000ms 2023-10-25 15:50:11 +01:00
Matt Aitken 237545754f Indexing errors weren’t being serialized to JSON properly in some cases, so the errors weren’t getting inserted
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 21s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-10-24 18:08:46 +01:00
Eric Allam d7c1e98e5e Update pnpm lock 2023-10-24 17:49:39 +01:00
github-actions[bot] 71a0afeb8d chore: Update version for release (#684)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-24 17:49:06 +01:00
Eric Allam 36a390449a @trigger.dev/openai: Upgrade to openai-node SDK 4.13.0, add support for request options and defaultQuery/defaultHeaders 2023-10-24 17:45:43 +01:00
James Ritchie 375e495423 Fixed accounts table not scrolling in the admin area 2023-10-24 17:34:33 +01:00
D-K-P 866c20cd39 Updated the Slack underlying client example 2023-10-24 16:26:14 +01:00
Eric Allam 408785b8ef Update pnpm lock file 2023-10-24 13:41:35 +01:00
github-actions[bot] f982e00997 chore: Update version for release (#618)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-24 13:40:57 +01:00
Eric Allam 7e100b8362 ignore: adding more logs to perform task operation
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 6s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
2023-10-24 12:14:52 +01:00
Eric Allam ea052c44c7 ignore: Add durationinMs to log 2023-10-24 11:38:35 +01:00
D-K-P 76224568f4 Airtable copy updates 2023-10-24 11:31:01 +01:00
Dan 72e67981b0 Merge pull request #666 from shelar1423/patch-3
Improved the Airtable integration documentation
2023-10-24 11:08:03 +01:00
Eric Allam cc44aff24b Added some docs about using the OpenAI integration as a universal client 2023-10-24 09:35:20 +01:00
Eric Allam 07d28ee4f5 Add custom baseURL and idempotency key support to OpenAI integration (#682)
This commit introduces the ability to set custom base URLs, which allows for the use of different APIs such as Perplexity. It also ensures that any OpenAI-related task now includes an idempotency key when making requests. Works for background tasks as well.
2023-10-24 09:19:38 +01:00
D-K-P f16a2cc4e6 Added link to the api reference repo to the readme 2023-10-24 09:11:57 +01:00
D-K-P b859837564 Edited the OpenAI ovrerview and added correct links for the tasks 2023-10-23 17:19:19 +01:00
Dan 1343003efa Merge pull request #673 from Rutam21/TRI-1430
[TRI-1430] : Improve the OpenAI integration documentation
2023-10-23 16:58:22 +01:00
Eric Allam 0c4aea0338 Add execution duration for background fetch 2023-10-23 16:55:00 +01:00
Eric Allam 627c767c64 fix: fix trigger sources getting repeatedly registered
Fixes a bug where trigger sources would get re-registered on every index, no matter if they were already properly registered or not. This was causing a lot of extra “internal” runs and being overly chatty with client endpoints
2023-10-23 15:24:35 +01:00
Digvijay Shelar f3a0c3a5d1 Merge branch 'main' into patch-3 2023-10-23 17:00:57 +05:30
dhselar1423 7bca3d9c15 made suggested changes 2023-10-23 16:57:52 +05:30
D-K-P 01fc1c6634 Slack integration docs copy updates 2023-10-23 12:17:29 +01:00
Dan 2d4f85e9f4 Merge pull request #667 from shelar1423/patch-4
Improved the Slack integration documentation
2023-10-23 12:08:40 +01:00
Rutam21 8718b28949 [TRI-1430] : Improve the OpenAI integration documentation 2023-10-21 04:55:12 +05:30
dhselar1423 2caa2a5bbd Merge branch 'patch-4' of https://github.com/shelar1423/trigger.dev into patch-4 2023-10-20 23:14:43 +05:30
dhselar1423 43a10ee695 slack fixes 2023-10-20 23:11:41 +05:30
Digvijay Shelar 018005d478 Merge branch 'main' into patch-4 2023-10-20 22:33:54 +05:30
dhselar1423 4da187c4c5 Merge branch 'patch-3' of https://github.com/shelar1423/trigger.dev into patch-3 2023-10-20 22:33:15 +05:30
dhselar1423 4925424778 Oauth and overview modifications 2023-10-20 22:32:48 +05:30
dhselar1423 7ef61222ed slack editing 2023-10-20 22:26:38 +05:30
Eric Allam 044d38e390 Improvement: Auto Execution Yielding (#612)
* Auto-yield run execution to help prevent duplicate task executions

* Add auto-yield config to endpoints

* Refactor run execution with buffer and limits

Introduced constants RUN_CHUNK_EXECUTION_BUFFER and MAX_RUN_CHUNK_EXECUTION_LIMIT. Adjusted PerformRunExecutionV2Service to use the new constants to fine-tune execution timings and buffers.

* Add endpoint probing functionality

Added new `RESPONSE_TIMEOUT_STATUS_CODES` in `consts.ts` to manage timeout responses. Additional functions `detectResponseIsTimeout(response: Response)` was added in `endpoint.server.ts` to detect if a response was a timeout based on the status codes from `RESPONSE_TIMEOUT_STATUS_CODES`.

Update actions to use new endpoint probing endpoint service. This allows for the early probing of endpoints to determine if they're up and running.

A new class `ProbeEndpointService` was created in `probeEndpoint.server.ts` which makes HTTP requests to a given endpoint and updates its properties based on the result.

Finally, `detectResponseIsTimeout(response)` is used in `performRunExecutionV2.server.ts` for marking the execution as succeeded when facing a timeout.

* Refactored probe method in EndpointApi class

The probe method of the EndpointApi class has been refactored to remove the error handling part and it now takes a timeout sent from the client directly. The corresponding changes were also made in the ProbeEndpointService and TriggerClient objects to reflect the alterations in the probe method.

The error handling related to the timeout has been removed and the responsibility of handling the timeout has been shifted to the client. Thus, the probe method has been greatly simplified. The `probeEndpoint.server.ts` file was also changed to accommodate the change in behavior of the probe result.

In the `triggerClient.ts` the timeout for probe is now read from the incoming request object. For backward compatibility, if no timeout is provided in the request, the default value of 15 minutes is used.

* Remove performRunExecution v1 enqueue function
* Better document limits and add docs on increasing function timeouts
* Upgrade webapp docker container to use 18.18.2
* force clients to yield when a run is executing in a gracefully shutting down worker
* Renamed task `key` to `cacheKey` and added more task documentation
* Index the `@trigger.dev/sdk` version on Endpoints
2023-10-20 17:44:10 +01:00
James Ritchie e7768881af Admin page now full screen with now extra UI 2023-10-20 16:53:04 +01:00
James Ritchie 5ea6a49d08 Made the app work very basically on mobile devices (#668)
* Removed the no-mobile blocker overlay so you can use the site on a phone

* Removed mobile dropdown from nav

* The app works on mobile at a width of 1024px
2023-10-20 16:12:17 +01:00
D-K-P dce5578327 Improved the GitHub overview copy 2023-10-20 14:35:28 +01:00
dhselar1423 fde3f17a25 fixed typos 2023-10-20 19:00:41 +05:30
dhselar1423 a6c9cac26c slack docs 2023-10-20 18:30:03 +05:30
Digvijay Shelar 960d99461a Merge branch 'triggerdotdev:main' into patch-3 2023-10-20 17:55:25 +05:30
dhselar1423 e5307133b3 airtable docs 2023-10-20 17:55:07 +05:30
D-K-P 34bb3acc23 Removed idempotencyKey line from GH tasks 2023-10-20 12:50:27 +01:00
Hemachandar 6d3b761cd7 fix: set correct next/prev cursors in run-list-presenter (#642)
* fix: set correct next/prev cursors in run-list-presenter

* set runs to return
2023-10-20 12:00:17 +01:00
Matt Aitken abc9737a4f Tabler icons added to NamedIcon
commit 13ed268f1d6732ef28c6e4a9eeb927c1af822032
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 11:38:22 2023 +0100

    Added all the tabler-icons to Storybook

commit 62738d71a40f17c45c0b29332359b2b670233a97
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 11:38:09 2023 +0100

    Use the no-stroke version of the tabler-sprite so we can control the stroke width

commit a1b7edd57f8b2d3f2a5f4d96cbc9eb0a887f3442
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 11:37:54 2023 +0100

    Set the default width to 1.5, so they’re a bit thinner

commit 69ba38802b849376611678537f7d8e248b60086b
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 10:55:34 2023 +0100

    Added a tabler icon to an events job catalog job

commit eb3c5f649d06adcc9ce0641f50e082ecdeead7a2
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 20 10:53:40 2023 +0100

    Moved the tabler sprite to the app and use an import. This means it’ll be cached and we can move it

commit c24d76e6df9233b001a0242f8806076562ca2e90
Author: Chaturved Degloorkar <48447253+chaturrved@users.noreply.github.com>
Date:   Fri Oct 20 15:22:59 2023 +0530

    feat: Add support for tabler-icons when using the icon for Tasks  (#629)

    * Add support for tabler-icons

    * Changed the core update type from minor to a patch

    ---------

    Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-20 11:41:45 +01:00
Bhargav Shirin Nalamati 84af64165c changed the title for contributors (#633) 2023-10-20 10:22:13 +01:00
Alexandre Costa 0adf41c7f0 Feat: add maxDuration setting for API/Trigger route in the CLI Init Command for Next.js (#617)
* add cli init maxDuration to the api/trigger route for nextjs

* refactor createTriggerRoute

* move boxen log to createTriggerRoute function

* refine detectNextVersion and versionNumberPattern regex to match the latest nextjs version

* add tests for the detectNextVersion function

* add changeset file

* If the regex doesn't match, it returns null instead of throwing

* Tweaked message about the max duration

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-20 10:16:44 +01:00
Dan b7f4e40edb Merge pull request #660 from Rutam21/TRI-1425
[TRI-1425] : Improve the Plain integration documentation.
2023-10-20 10:05:18 +01:00
Rutam21 cf33396dc0 [TRI-1425] Improve the Plain integration documentation 2023-10-20 02:45:46 +05:30
Dan 51f2cd6b3f Updated posthog to 1.83.0 (#608) 2023-10-19 16:34:41 +01:00
vimode 513d0f071d fix: broken links in documentation (#602)
* fix: broken links at what is triggerdev docs

* fix: broken links for apikey

* fix: broken sdk link

* fix: broken link in concepts/triggers

* fix: broken link for zod guide at concepts/triggers/events

* fix: broken links in title for projects

* Shouldn't have /docs at the start

* Use backgroundFetch path

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-19 16:27:21 +01:00
Hemachandar 50e3192453 feat: add ability to use custom tunnel in dev cmd (#597)
* feat: add ability to use custom tunnel in dev cmd

* Added a short flag -t for the tunnel flag

* skip framework url resolution if tunnel-url is provided

* remove type annotation

---------

Co-authored-by: Eric Allam <eric@trigger.dev>
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-19 16:11:08 +01:00
Dan 1524c95a7a Merge pull request #650 from shelar1423/patch2
Get help icon and twitter text modifications
2023-10-19 12:46:41 +01:00
Eric Allam c8aaea8ad0 Improvements: Gracefully shutdown to prevent locked jobs (#648)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 6s
🚀 Publish Trigger.dev Docker / units (push) Failing after 5s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 6s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* WIP

* Report ECS task info on startup and shutdown

* Fixed lifecycle name

* Re-add terminus

* Require the build dir when http server is disabled

* Remove unnecessary logs

* Implement graceful shutdown in ZodWorker

* Re-order some code

* Increase the keepAliveTimeout to 65 seconds to prevent LB 502 errors
2023-10-19 11:18:19 +01:00
dhselar1423 cf7f25abcd fix 2023-10-19 08:08:53 +05:30
dhselar1423 99453e8581 Merge branch 'patch2' of https://github.com/shelar1423/trigger.dev into patch2 2023-10-19 01:10:16 +05:30
dhselar1423 bc7206d981 x 2023-10-19 01:10:05 +05:30
Digvijay Shelar 64b8e150ea Merge branch 'triggerdotdev:main' into patch2 2023-10-19 01:03:19 +05:30
dhselar1423 b3b5852a88 fixed icons 2023-10-19 01:02:54 +05:30
Digvijay Shelar d1ecd6b99c framework icons (#635) 2023-10-17 22:00:56 +01:00
dhselar1423 0fb16a7324 framework icons 2023-10-18 01:21:47 +05:30
Matt Aitken 4f317055fd useDevEnvironment returns the current user’s dev environment, not the first 2023-10-16 16:19:16 +01:00
Eric Allam 3afd5f8cf3 Merge branch 'main' of github.com:triggerdotdev/trigger.dev 2023-10-16 16:04:09 +01:00
Eric Allam c922d24941 Adding simulate graphile job to easily debug graceful shutdown retries in docker image 2023-10-16 16:04:05 +01:00
Matt Aitken 39ec34f84f Latest lockfile 2023-10-16 13:16:02 +01:00
Ravan b9eba68078 feat: export base file (#610)
* feat: export base file

* Added a change set

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-13 17:44:05 +01:00
nicktrn 9df93d0798 CLI create-integration templates and shared configs (#511)
* Add tsup config package

* Add integration tsconfig to extend from

* Return correct version via cli -v

* Make create-integration use templates

* Add changeset

* Switch to liquidjs templates

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-13 16:55:22 +01:00
Matt Aitken 6769d6b439 Squashed commit of the following:
commit c028db229cece2bf7d7acf04e7b1c08fea61798c
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 16:04:54 2023 +0100

    Some fixes for getting the endpoint id

commit e31b7ac5f2d451f70e3d42a3fb3e5f718d93485b
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 15:39:18 2023 +0100

    Added a link in the instructions if Node isn’t detected

commit 1a59cced775fbeac3e5b94e0c151b490c838d8f8
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 15:39:05 2023 +0100

    Fixed some conflict issues

commit 77263ed26bcf7025071ac0faf15fb867fff245d0
Author: Matt Aitken <matt@mattaitken.com>
Date:   Fri Oct 13 15:38:56 2023 +0100

    Added a changeset

commit 1a7df3f37f0c65d68cb9a9fe7ac50328bbb4d879
Author: GuyBorderless <100129659+guy-borderless@users.noreply.github.com>
Date:   Fri Oct 13 17:31:01 2023 +0300

    feat: enable deno  (#531)

    * adding deno reference project and JSRuntime file

    * accessing runtime specific functions through JsRuntime

    * adding support for both deno.json and deno.jsonc

    * no automatic setup for deno projects currently

    ---------

    Co-authored-by: Guy Kedem <guy@propertiz.net>
    Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-10-13 16:20:18 +01:00
Eric Allam 70410d33af Ability to change the timeout of the graceful shutdown via env var 2023-10-13 15:12:05 +01:00
Hemachandar 6371dd4f95 feat: allow sendEvent to update an existing event (#607) 2023-10-13 14:46:35 +01:00
325 changed files with 14522 additions and 4255 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"recommendations": [
"astro-build.astro-vscode",
"denoland.vscode-deno"
],
"unwantedRecommendations": [
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"deno.enablePaths": ["references/deno-reference"]
}
+43
View File
@@ -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).
+2 -2
View File
@@ -19,7 +19,7 @@
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) 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.
- 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._
@@ -96,7 +96,7 @@ We provide an official trigger.dev docker image you can use to easily self-host
To setup and develop locally or contribute to the open source project, follow our [development guide](./CONTRIBUTING.md).
## 🙏 to our contributors
## 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" />
+2 -1
View File
@@ -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 });
},
@@ -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>
);
};
@@ -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);
@@ -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>
);
}
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>
);
}
+3
View File
@@ -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];
+80 -56
View File
@@ -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);
});
}
+1
View File
@@ -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>;
+9 -5
View File
@@ -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,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();
}
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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;
+6 -6
View File
@@ -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,
+3 -3
View File
@@ -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";
+3 -3
View File
@@ -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;
+4 -4
View File
@@ -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;
+3 -3
View File
@@ -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(
+101
View File
@@ -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";
}
}
}
+12
View File
@@ -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"
);
}
@@ -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;
+1 -25
View File
@@ -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";
}
}
}
+3 -2
View File
@@ -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,
};
}
@@ -102,6 +102,7 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
recurringTasks?: ZodRecurringTasks;
cleanup?: ZodWorkerCleanupOptions;
reporter?: ZodWorkerReporter;
shutdownTimeoutInMs?: number;
};
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
@@ -114,6 +115,8 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
#runner?: GraphileRunner;
#cleanup: ZodWorkerCleanupOptions | undefined;
#reporter?: ZodWorkerReporter;
#shutdownTimeoutInMs?: number;
#shuttingDown = false;
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
this.#name = options.name;
@@ -124,6 +127,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#recurringTasks = options.recurringTasks;
this.#cleanup = options.cleanup;
this.#reporter = options.reporter;
this.#shutdownTimeoutInMs = options.shutdownTimeoutInMs ?? 60000; // default to 60 seconds
}
get graphileWorkerSchema() {
@@ -143,6 +147,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#runner = await graphileRun({
...this.#runnerOptions,
noHandleSignals: true,
taskList: this.#createTaskListFromTasks(),
parsedCronItems,
});
@@ -199,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();
}
@@ -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
View File
@@ -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 });
},
@@ -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";
@@ -34,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);
@@ -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);
@@ -2,8 +2,12 @@ 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, CalloutVariant } from "~/components/primitives/Callout";
@@ -20,12 +24,6 @@ import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server";
import { endpointStreamingPath } from "~/utils/pathBuilder";
import { EndpointIndexStatus, RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
import {
EndpointIndexStatusIcon,
EndpointIndexStatusLabel,
endpointIndexStatusTitle,
} from "~/components/environments/EndpointIndexStatus";
import { CodeBlock } from "~/components/code/CodeBlock";
type ConfigureEndpointSheetProps = {
slug: string;
@@ -39,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 });
},
@@ -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 });
},
@@ -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 { 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,
@@ -44,8 +44,9 @@ import { requestUrl } from "~/utils/requestUrl.server";
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
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);
@@ -148,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
@@ -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);
@@ -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";
@@ -21,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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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();
@@ -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);
@@ -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";
@@ -38,7 +38,7 @@ 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);
@@ -166,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 });
},
@@ -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);
@@ -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>
)}
@@ -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>
)}
@@ -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 });
},
@@ -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);
@@ -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);
@@ -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();
@@ -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 });
},
+2 -4
View File
@@ -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 />
+69 -78
View File
@@ -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 });
}
+6 -165
View File
@@ -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", {
@@ -1,4 +1,4 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
@@ -11,7 +11,7 @@ const ParamsSchema = z.object({
id: z.string(),
});
export async function loader({ request, params }: LoaderArgs) {
export async function loader({ request, params }: LoaderFunctionArgs) {
logger.info("Fetching auth", { url: request.url });
const parsedParams = ParamsSchema.safeParse(params);
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -11,7 +11,7 @@ const ParamsSchema = z.object({
key: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
logger.info("Unregistering schedule", { url: request.url });
// Ensure this is a POST request
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { RegisterScheduleBodySchema, RegisterScheduleResponseBodySchema } from "@trigger.dev/core";
import { z } from "zod";
@@ -11,7 +11,7 @@ const ParamsSchema = z.object({
id: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
logger.info("Initializing schedule", { url: request.url });
// Ensure this is a POST request
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { UpdateTriggerSourceBodyV1Schema } from "@trigger.dev/core";
import { z } from "zod";
@@ -11,7 +11,7 @@ const ParamsSchema = z.object({
id: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
logger.info("Updating source", { url: request.url });
// Ensure this is a POST request
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { RegisterTriggerBodySchemaV1 } from "@trigger.dev/core";
import { z } from "zod";
@@ -12,7 +12,7 @@ const ParamsSchema = z.object({
key: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
logger.info("Registering trigger", { url: request.url });
// Ensure this is a POST request
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { InitializeTriggerBodySchema } from "@trigger.dev/core";
import { z } from "zod";
@@ -11,7 +11,7 @@ const ParamsSchema = z.object({
id: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
logger.info("Initializing trigger", { url: request.url });
// Ensure this is a POST request
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
CreateExternalConnectionBody,
@@ -16,7 +16,7 @@ const ParamsSchema = z.object({
clientSlug: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,4 +1,4 @@
import { ActionArgs, json } from "@remix-run/server-runtime";
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import {
EndpointIndexErrorSchema,
GetEndpointIndexResponse,
@@ -14,7 +14,7 @@ const ParamsSchema = z.object({
indexId: z.string(),
});
export async function loader({ request, params }: ActionArgs) {
export async function loader({ request, params }: ActionFunctionArgs) {
if (request.method.toUpperCase() !== "GET") {
return { status: 405, body: "Method Not Allowed" };
}
@@ -1,4 +1,4 @@
import { ActionArgs, json } from "@remix-run/server-runtime";
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -14,7 +14,7 @@ const BodySchema = z.object({
data: z.any().optional(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,4 +1,4 @@
import { ActionArgs, LoaderArgs, json } from "@remix-run/server-runtime";
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
@@ -12,7 +12,7 @@ const ParamsSchema = z.object({
indexHookIdentifier: z.string(),
});
export async function loader({ params }: LoaderArgs) {
export async function loader({ params }: LoaderFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
@@ -39,7 +39,7 @@ export async function loader({ params }: LoaderArgs) {
});
}
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { generateErrorMessage } from "zod-error";
@@ -11,7 +11,7 @@ const BodySchema = z.object({
id: z.string(),
});
export async function action({ request }: ActionArgs) {
export async function action({ request }: ActionFunctionArgs) {
logger.info("action", { url: request.url });
// Ensure this is a POST request
@@ -0,0 +1,49 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CancelEventService } from "~/services/events/cancelEvent.server";
import { logger } from "~/services/logger.server";
import { CancelRunsForEventService } from "~/services/events/cancelRunsForEvent.server";
const ParamsSchema = z.object({
eventId: z.string(),
});
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const authenticatedEnv = authenticationResult.environment;
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return json({ error: "Invalid or Missing eventId" }, { status: 400 });
}
const { eventId } = parsed.data;
const service = new CancelRunsForEventService();
try {
const res = await service.call(authenticatedEnv, eventId);
if (!res) {
return json({ error: "Event not found" }, { status: 404 });
}
return json(res);
} catch (err) {
logger.error("CancelRunsForEventService.call() error", { error: err });
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { eventRecordToApiJson } from "~/api.server";
@@ -10,7 +10,7 @@ const ParamsSchema = z.object({
eventId: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,4 +1,4 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { GetEvent } from "@trigger.dev/core";
import { z } from "zod";
@@ -10,7 +10,7 @@ const ParamsSchema = z.object({
eventId: z.string(),
});
export async function loader({ request, params }: LoaderArgs) {
export async function loader({ request, params }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { SendEventBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
@@ -6,7 +6,7 @@ import { authenticateApiRequest } from "~/services/apiAuth.server";
import { IngestSendEvent } from "~/services/events/ingestSendEvent.server";
import { eventRecordToApiJson } from "~/api.server";
export async function action({ request }: ActionArgs) {
export async function action({ request }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,4 +1,4 @@
import { LoaderArgs, json } from "@remix-run/server-runtime";
import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { resolveApiConnection } from "~/models/runConnection.server";
@@ -10,7 +10,7 @@ const ParamsSchema = z.object({
connectionId: z.string(),
});
export async function loader({ request, params }: LoaderArgs) {
export async function loader({ request, params }: LoaderFunctionArgs) {
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
@@ -1,4 +1,4 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
@@ -14,7 +14,7 @@ const SearchQuerySchema = z.object({
take: z.coerce.number().default(20),
});
export async function loader({ request, params }: LoaderArgs) {
export async function loader({ request, params }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { PrismaErrorSchema } from "~/db.server";
import { z } from "zod";
@@ -10,7 +10,7 @@ const ParamsSchema = z.object({
runId: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,5 +1,5 @@
import type { Organization, RuntimeEnvironment } from "@trigger.dev/database";
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import type { LogMessage } from "@trigger.dev/core";
import { LogMessageSchema } from "@trigger.dev/core";
@@ -13,7 +13,7 @@ const ParamsSchema = z.object({
runId: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,10 +1,7 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TaskStatus } from "@trigger.dev/database";
import {
RunTaskBodyOutput,
RunTaskBodyOutputSchema,
ServerTask,
JobRunStatusRecordSchema,
StatusHistory,
StatusHistorySchema,
StatusUpdate,
@@ -14,19 +11,15 @@ import {
} from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
const ParamsSchema = z.object({
runId: z.string(),
id: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "PUT") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,4 +1,4 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
import { z } from "zod";
@@ -13,7 +13,7 @@ const ParamsSchema = z.object({
const RecordsSchema = z.array(JobRunStatusRecordSchema);
export async function loader({ request, params }: LoaderArgs) {
export async function loader({ request, params }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
@@ -1,4 +1,4 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
@@ -12,7 +12,7 @@ const ParamsSchema = z.object({
secret: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -1,9 +1,9 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import { CompleteTaskBodyInputSchema } from "@trigger.dev/core";
import { z } from "zod";
import { PrismaClient, prisma } from "~/db.server";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -14,7 +14,7 @@ const ParamsSchema = z.object({
id: z.string(),
});
export async function action({ request, params }: ActionArgs) {
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
@@ -72,9 +72,9 @@ export async function action({ request, params }: ActionArgs) {
}
export class CompleteRunTaskService {
#prismaClient: PrismaClient;
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClient = prisma) {
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
@@ -86,7 +86,7 @@ export class CompleteRunTaskService {
): Promise<ServerTask | undefined> {
// Using a transaction, we'll first check to see if the task already exists and return if if it does
// If it doesn't exist, we'll create it and return it
const task = await this.#prismaClient.$transaction(async (tx) => {
const task = await $transaction(this.#prismaClient, async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
id,
@@ -152,6 +152,7 @@ export class CompleteRunTaskService {
},
include: {
attempts: true,
run: true,
},
});
});

Some files were not shown because too many files have changed in this diff Show More