Compare commits

...

53 Commits

Author SHA1 Message Date
Eric Allam 6979eb6e66 Fixed pnpm lock 2023-08-24 15:27:08 +01:00
github-actions[bot] b0f926d28d chore: Update version for release (#375)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-24 15:26:42 +01:00
Eric Allam 82d8e7d3f3 Added the new tasks to the openai docs 2023-08-24 15:24:30 +01:00
James Ritchie a214411218 Changed the button text for Delete Job on the settings page
🚀 Publish Trigger.dev Docker / ʦ TypeScript (push) Has been cancelled
🚀 Publish Trigger.dev Docker / Unit Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / e2e Tests (push) Has been cancelled
🚀 Publish Trigger.dev Docker / publish (push) Has been cancelled
2023-08-24 15:04:28 +01:00
James Ritchie 2806cee7de Conditionally show the disable Job text by the Delete button 2023-08-24 14:36:02 +01:00
James Ritchie bd28556bf3 Made the loading spinner white 2023-08-24 14:25:59 +01:00
James Ritchie 19a8d39fb0 The Delete button displays full width again 2023-08-24 14:11:55 +01:00
Chigala a907e2a988 chore: updated the type in the eventId argument (#389)
* chore: updated the type in the eventId argument

* Create nervous-masks-rest.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-24 11:51:06 +01:00
Eric Allam 403acac5ef #344 Upgrading the openai package to v4 (#383)
* #344 Upgrading the openai package to v4

* Create neat-roses-wait.md

* Fixed a couple of issues with the openai v4 upgrade

* Added fine tuning job tasks
2023-08-24 11:48:39 +01:00
James Ritchie 708ebbde14 Features: Disabling and deleting jobs (#381)
* Don’t show the Ready To Run Job prompt if you have an Integration that needs attention

* Don’t highlight the row red or green

* Improvements to the contrast of the app sections and dividers

* Removed the Jobs page title as it’s duped info

* using the new border variable

* Sticky last table cell

* Improved the sticky last table cell

* Make any last cell in a table sticky by adding isSticky to it

* Added a dropdown menu to the menu table cell

* table rows can be marked as disabled by adding disabled

* Using the jobTestPath function for the test path

* tidy up imports

* Removed un-used props

* Removed the green badge variant

* Added a new status badge to the Runs table

* Clicking the gradient clicks the row

* Delete Job triggers a modal popup

* Added a large danger button type

* Added some modal styling and started adding data

* Added more styling and data to the delete job modal

* A table can now be given a full width prop

* Large danger button added to Storybook

* Danger button disabled state looks disabled now

* New active badge component to display in the table and logic for showing the env data

* Style updates to the dialog component

* active and job status badges can now have a small size

* Added a new named icon

* The Job page shows the Job status in the PageInfoRow

* Small badge style update

* Runs table has a sticky right cell

* Created a JobStatusTable component

* Added some placeholder help panel content for disabling a Job

* WIP creating a Settings page

* Added a delete button that triggers the delete modal – just need data hooking up

* Implemented deleting jobs from the dashboard

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-24 10:30:00 +01:00
Eric Allam 3ce5397072 Implement the disable job feature backend when indexing endpoints (#382) 2023-08-23 10:30:06 +01:00
Liran Tal ee79ab30a9 feat: introduce support for native astro integration package (#354)
* feat: introduce support for native astro integration package

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: update license copyright to match other packages from trigger

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: doc update to refer to the official astro package from trigger

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: repo URLs update accordingly to official repo

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* fix: remove myself as author of the package to not confuse people

Signed-off-by: Liran Tal <liran.tal@gmail.com>

* Remove package-lock because this is now in the pnpm monorepo

---------

Signed-off-by: Liran Tal <liran.tal@gmail.com>
Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-23 10:29:10 +01:00
Eric Allam 2b21f5ba8a bugfix: specifiying an alternate WORKER_SCHEMA now works 2023-08-22 13:15:12 +01:00
Eric Allam 8c27236bc9 Merge branch 'rishi-raj-jain-main' 2023-08-22 11:51:02 +01:00
Eric Allam b8d191052b A couple tweaks to the update command 2023-08-22 11:50:52 +01:00
Eric Allam 979f5f88ae Merge branch 'main' of https://github.com/rishi-raj-jain/trigger.dev into rishi-raj-jain-main
# Conflicts:
#	pnpm-lock.yaml
2023-08-22 11:32:31 +01:00
Eric Allam fef1670533 Improved the testing CLI contributing guide 2023-08-22 11:28:03 +01:00
Eric Allam 3897e6e695 @trigger.dev/cli ini: Make it more clear which API key the init command expects 2023-08-22 11:16:29 +01:00
Eric Allam fc083de102 Merge branch 'Chigala-bugfix/cli-init' 2023-08-22 11:06:44 +01:00
Eric Allam c7c5fd7fac Fixed the examples job path 2023-08-22 11:06:32 +01:00
Eric Allam 4630960478 Merge branch 'bugfix/cli-init' of https://github.com/Chigala/trigger.dev into Chigala-bugfix/cli-init 2023-08-22 10:57:05 +01:00
Eric Allam 302bd02ff1 Issue #377: only expose the external eventId in the API (#380)
* Issue #377: only expose the external eventId in the API

* Create eighty-zebras-bow.md
2023-08-22 10:46:46 +01:00
Eric Allam b5db9f5e5c Added MIT license to all public packages 2023-08-21 22:38:11 +01:00
Rishi Raj Jain 4b64c2d020 remove Index type 2023-08-22 02:19:36 +05:30
Rishi Raj Jain 0c01e58db9 await 2023-08-22 02:15:43 +05:30
Rishi Raj Jain 367ce2e412 use exitsing methods 2023-08-22 02:07:14 +05:30
Rishi Raj Jain 92f80fd87c Merge branch 'triggerdotdev:main' into main 2023-08-22 02:03:43 +05:30
Chigala 135cb4921f added changeset 2023-08-21 19:02:52 +01:00
Chigala 6cbc4f1190 fix: CLI init now logs the correct app route path. 2023-08-21 18:59:31 +01:00
Rishi Raj Jain 1bf84548d7 add types 2023-08-21 23:19:03 +05:30
Rishi Raj Jain ce95744e98 use npm-check-updates 2023-08-21 23:11:15 +05:30
Eric Allam 0493cebaed Merge branch 'Chigala-feat/cancelEvent-function' 2023-08-21 17:49:48 +01:00
Eric Allam ac98219b76 adding changeset 2023-08-21 17:49:28 +01:00
Eric Allam c8ddc19d5a Fixed an issue with IngestSendEvent not returning an existing event 2023-08-21 17:48:34 +01:00
Eric Allam c6df574689 Fixed io.cancelEvent (shouldn’t be a noop) 2023-08-21 17:48:21 +01:00
Rishi Raj Jain 232b7cd26a Merge branch 'triggerdotdev:main' into main 2023-08-21 21:49:35 +05:30
Eric Allam b9dc7ce89e Merge branch 'feat/cancelEvent-function' of https://github.com/Chigala/trigger.dev into Chigala-feat/cancelEvent-function
# Conflicts:
#	apps/webapp/app/platform/zodWorker.server.ts
2023-08-21 16:57:36 +01:00
Chigala 8cf85443d5 bugfix: jsx extensions breaks cli init (#374)
* bugfix: jsx extensions breaks cli init

* Update init.ts (adding ts extension to check)

* Create rude-spoons-compete.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-21 16:03:10 +01:00
Hugo Nogueira dd10717628 feat: add hostname option to the dev command (#370)
* feat: add hostname option to the dev command

- This commit adds a `hostname` option to the cli `dev` command, to allow the cli to point to nextjs applications running on different hostnames other than `localhost`. Example: the nextjs app was started using a --hostname 0.0.0.0 option to be able to be visible from inside docker containers. So adding --hostname 0.0.0.0 to `trigger-cli dev` would make it work.

* docs: add dev cli command hostname option documentation

* feat: add hostname option to the dev command

- This commit adds a `hostname` option to the cli `dev` command, to allow the cli to point to nextjs applications running on different hostnames other than `localhost`. Example: the nextjs app was started using a --hostname 0.0.0.0 option to be able to be visible from inside docker containers. So adding --hostname 0.0.0.0 to `trigger-cli dev` would make it work.

* docs: add dev cli command hostname option documentation

* Update hip-coins-reply.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2023-08-21 15:46:16 +01:00
Chigala 18a02829e3 set the jobkey as event:event.id 2023-08-18 15:21:09 +01:00
Matt Aitken 65cff19ead Merge branch 'main' into main 2023-08-18 11:20:44 +01:00
Chigala ef1a1ff343 added io.cancelEvent support and fixed other requested changes 2023-08-18 06:21:57 +01:00
Rishi Raj Jain 2fbaa30b9f Update index.ts 2023-08-17 23:45:44 +05:30
Rishi Raj Jain 1199e934c2 Merge branch 'main' into main 2023-08-17 23:45:12 +05:30
Chigala a3278cb8a2 bug fixes 2023-08-15 00:16:38 +01:00
Rishi Raj Jain 05b5ed6973 Update update.ts 2023-08-14 21:42:08 +05:30
Rishi Raj Jain 859425f093 Update update.ts 2023-08-14 21:32:55 +05:30
Chigala 24c8f08267 feat: added the ability to cancel Events based on the event ID 2023-08-14 14:56:48 +01:00
Rishi Raj Jain 3159619852 revert pnpm changes 2023-08-12 11:28:27 +05:30
Rishi Raj Jain 300c102b31 remvoe axios and use node-fetch 2023-08-12 11:26:08 +05:30
Rishi Raj Jain 9e0b152c0e Update package.json 2023-08-12 11:19:18 +05:30
Rishi Raj Jain 4e78da319c Create swift-eagles-float.md 2023-08-11 23:54:06 +05:30
Rishi Raj Jain 2c37091b08 complete the script 2023-08-11 23:48:37 +05:30
142 changed files with 4121 additions and 896 deletions
+12 -10
View File
@@ -136,38 +136,40 @@ pnpm run dev
3. Create a new temporary Next.js app in examples directory
```sh
pnpm create next-app@latest
cd ./examples
pnpm create next-app@latest test-cli --ts --no-eslint --tailwind --app --src-dir --import-alias "@/*"
```
Follow the prompts to create a TypeScript project using the App Directory.
4. Then once that's finished, add the `@trigger.dev/cli` to the `devDependencies` of the newly created Next.js app's `package.json` file, like so:
```json
{
// other package.json properties
"devDependencies": { "@trigger.dev/cli": "workspace:*" }
}
```
5. Open a new terminal window, navigate into the example, and initialize the CLI:
5. Back in the terminal, navigate into the example, and initialize the CLI. When prompted, select `self-hosted` and enter `localhost:3030` if you are testing against the local instance of Trigger.dev, or you can just use the Trigger.dev cloud. When asked for an API key, use the key you copied earlier.
```sh
cd examples/your-newly-created-nextjs-project
cd ./test-cli
pnpm i
pnpm exec trigger-cli init
```
6. When prompted, select `self-hosted` and enter `localhost:3030` for your local version of the webapp. When asked for an API key, use the key you copied earlier.
6. If you are just testing the `init` command, you can stop here. If you'd like to test the `dev` command, first start the Next.js app on port 3000:
7. Run the CLI
```sh
pnpm run dev
```
7. Open a new terminal window, and then run the `dev` command like so:
```sh
pnpm exec trigger-cli dev
```
8. After running the CLI, start your newly created Next.js project. You should now be able to see the changes.
9. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
8. Please remember to delete the temporary project you created after you've tested the changes, and before you raise a PR.
## Running end-to-end webapp tests
+157 -157
View File
@@ -2,180 +2,180 @@
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
@@ -186,16 +186,16 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright [2023] [Trigger.dev]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+15
View File
@@ -0,0 +1,15 @@
import { ApiEventLog } from "@trigger.dev/core";
import { EventRecord } from "@trigger.dev/database";
export function eventRecordToApiJson(eventRecord: EventRecord): ApiEventLog {
return {
id: eventRecord.eventId,
name: eventRecord.name,
payload: eventRecord.payload as any,
context: eventRecord.context as any,
timestamp: eventRecord.timestamp,
deliverAt: eventRecord.deliverAt,
deliveredAt: eventRecord.deliveredAt,
cancelledAt: eventRecord.cancelledAt,
};
}
@@ -0,0 +1,57 @@
import { cn } from "~/utils/cn";
const variant = {
small:
"py-[0.25rem] px-1.5 text-xxs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-[0.125rem]",
normal:
"py-1 px-1.5 text-xs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-sm",
};
type ActiveBadgeProps = {
active: boolean;
className?: string;
badgeSize?: keyof typeof variant;
};
export function ActiveBadge({ active, className, badgeSize = "normal" }: ActiveBadgeProps) {
switch (active) {
case true:
return (
<span className={cn(variant[badgeSize], "bg-slate-800 text-green-500", className)}>
Active
</span>
);
case false:
return (
<span className={cn(variant[badgeSize], "bg-slate-800 text-dimmed", className)}>
Disabled
</span>
);
}
}
export function MissingIntegrationBadge({
className,
badgeSize = "normal",
}: {
className?: string;
badgeSize?: keyof typeof variant;
}) {
return (
<span className={cn(variant[badgeSize], "bg-rose-600 text-white", className)}>
Missing Integration
</span>
);
}
export function NewBadge({
className,
badgeSize = "normal",
}: {
className?: string;
badgeSize?: keyof typeof variant;
}) {
return (
<span className={cn(variant[badgeSize], "bg-green-600 text-background", className)}>New!</span>
);
}
@@ -0,0 +1,54 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import {
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { EnvironmentLabel } from "./environments/EnvironmentLabel";
import { DateTime } from "./primitives/DateTime";
import { ActiveBadge } from "./ActiveBadge";
export type JobEnvironment = {
type: RuntimeEnvironmentType;
lastRun?: Date;
version: string;
enabled: boolean;
};
type JobStatusTableProps = {
environments: JobEnvironment[];
};
export function JobStatusTable({ environments }: JobStatusTableProps) {
return (
<Table fullWidth>
<TableHeader>
<TableRow>
<TableHeaderCell>Env</TableHeaderCell>
<TableHeaderCell>Last Run</TableHeaderCell>
<TableHeaderCell alignment="right">Version</TableHeaderCell>
<TableHeaderCell alignment="right">Status</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{environments.map((environment, index) => (
<TableRow key={index}>
<TableCell>
<EnvironmentLabel environment={environment} />
</TableCell>
<TableCell>
{environment.lastRun ? <DateTime date={environment.lastRun} /> : "Never Run"}
</TableCell>
<TableCell alignment="right">{environment.version}</TableCell>
<TableCell alignment="right">
<ActiveBadge active={environment.enabled} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
@@ -13,7 +13,7 @@ export function EnvironmentLabel({
return (
<span
className={cn(
"flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
"inline-flex h-4 items-center justify-center rounded-[2px] px-1 text-xxs font-medium uppercase tracking-wider text-midnight-900",
environmentColorClassName(environment),
className
)}
@@ -414,6 +414,60 @@ export function HowToUseThisIntegration({ integration, help, integrationClient }
);
}
export function HowToDisableAJob({
id,
name,
version,
}: {
id: string;
name: string;
version: string;
}) {
return (
<>
<Paragraph spacing>
To disable a job, you need to set the <InlineCode>enabled</InlineCode> property to{" "}
<InlineCode>false</InlineCode>.
</Paragraph>
<StepNumber
stepNumber="1"
title={
<>
Set <InlineCode>enabled</InlineCode> to <InlineCode>false</InlineCode>
</>
}
/>
<StepContentContainer>
<CodeBlock
showLineNumbers={false}
className="mb-4"
code={`client.defineJob({
id: "${id}",
name: "${name}",
version: "${version}",
enabled: false,
// ...rest of your Job definition
});`}
/>
</StepContentContainer>
<StepNumber
stepNumber="2"
title={
<>
Run the <InlineCode>@trigger.dev/cli dev</InlineCode> command
</>
}
/>
<StepContentContainer>
<Paragraph spacing>
If you aren't already running the <InlineCode>dev</InlineCode> command, run it now.
</Paragraph>
<TriggerDevCommand />
</StepContentContainer>
</>
);
}
export function HowToUseApiKeysAndEndpoints() {
return (
<>
@@ -0,0 +1,109 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
import { JobStatusTable } from "../JobsStatusTable";
import { Button } from "../primitives/Buttons";
import { Header1, Header2 } from "../primitives/Headers";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import { TextLink } from "../primitives/TextLink";
import { useFetcher } from "@remix-run/react";
import { Spinner } from "../primitives/Spinner";
type JobEnvironment = {
type: RuntimeEnvironmentType;
lastRun?: Date;
version: string;
enabled: boolean;
};
type DeleteJobDialogContentProps = {
id: string;
title: string;
slug: string;
environments: JobEnvironment[];
redirectTo?: string;
};
export function DeleteJobDialogContent({
title,
slug,
environments,
id,
redirectTo,
}: DeleteJobDialogContentProps) {
const canDelete = environments.every((environment) => !environment.enabled);
const fetcher = useFetcher();
const isLoading =
fetcher.state === "submitting" ||
(fetcher.state === "loading" && fetcher.formMethod === "DELETE");
return (
<div className="flex w-full flex-col items-center gap-y-6">
<div className="flex flex-col items-center justify-center gap-y-2">
<Header1>{title}</Header1>
<Paragraph variant="small">ID: {slug}</Paragraph>
</div>
<JobStatusTable environments={environments} />
<Header2
className={cn(
canDelete ? "border-rose-500 bg-rose-500/10" : "border-amber-500 bg-amber-500/10",
"rounded border px-3.5 py-2 text-center text-bright"
)}
>
{canDelete
? "Are you sure you want to delete this Job?"
: "You can't delete this Job until all env are disabled"}
</Header2>
<Paragraph variant="small" className="px-6 text-center">
{canDelete ? (
<>
This will permanently delete the Job <span className="strong text-bright">{title}</span>
. This includes the deletion of all Run history. This cannot be undone.
</>
) : (
<>
This Job is still active in an environment. You need to disable it in your Job code
first before it can be deleted.{" "}
<TextLink to="https://trigger.dev/docs/documentation/guides/jobs/managing#disabling-jobs">
Learn how to disable a Job
</TextLink>
.
</>
)}
</Paragraph>
{canDelete ? (
<fetcher.Form
method="delete"
action={`/resources/jobs/${id}${redirectTo ? `?redirectTo=${redirectTo}` : ""}`}
className="w-full"
>
<Button variant="danger/large" fullWidth>
{isLoading ? (
<Spinner color="white" />
) : (
<>
<NamedIcon
name="trash-can"
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
/>
Delete this Job
</>
)}
</Button>
</fetcher.Form>
) : (
<Button variant="danger/large" fullWidth disabled>
<>
<NamedIcon
name="trash-can"
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
/>
Delete this Job
</>
</Button>
)}
</div>
);
}
@@ -0,0 +1,29 @@
import { ActiveBadge, MissingIntegrationBadge, NewBadge } from "../ActiveBadge";
type JobStatusBadgeProps = {
enabled: boolean;
hasIntegrationsRequiringAction: boolean;
hasRuns: boolean;
badgeSize?: "small" | "normal";
};
export function JobStatusBadge({
enabled,
hasIntegrationsRequiringAction,
hasRuns,
badgeSize = "normal",
}: JobStatusBadgeProps) {
if (!enabled) {
return <ActiveBadge active={false} badgeSize={badgeSize} />;
}
if (hasIntegrationsRequiringAction) {
return <MissingIntegrationBadge badgeSize={badgeSize} />;
}
if (!hasRuns) {
return <NewBadge badgeSize={badgeSize} />;
}
return <ActiveBadge active={true} badgeSize={badgeSize} />;
}
+46 -22
View File
@@ -1,26 +1,29 @@
import { jobPath } from "~/utils/pathBuilder";
import { ProjectJob } from "~/hooks/useJobs";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { JobRunStatus } from "~/models/job.server";
import { jobPath, jobTestPath } from "~/utils/pathBuilder";
import { Button } from "../primitives/Buttons";
import { DateTime } from "../primitives/DateTime";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog";
import { LabelValueStack } from "../primitives/LabelValueStack";
import { NamedIcon } from "../primitives/NamedIcon";
import { Paragraph } from "../primitives/Paragraph";
import { PopoverMenuItem } from "../primitives/Popover";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
} from "../primitives/Table";
import { SimpleTooltip } from "../primitives/Tooltip";
import { runStatusTitle } from "../runs/RunStatuses";
import { ProjectJob } from "~/hooks/useJobs";
import { useProject } from "~/hooks/useProject";
import { useOrganization } from "~/hooks/useOrganizations";
import { JobRunStatus } from "~/models/job.server";
import { cn } from "~/utils/cn";
import { Badge } from "../primitives/Badge";
import { DeleteJobDialogContent } from "./DeleteJobModalContent";
import { JobStatusBadge } from "./JobStatusBadge";
export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) {
const organization = useOrganization();
@@ -35,6 +38,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
<TableHeaderCell>Integrations</TableHeaderCell>
<TableHeaderCell>Properties</TableHeaderCell>
<TableHeaderCell>Last run</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
</TableHeader>
@@ -43,13 +47,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
jobs.map((job) => {
const path = jobPath(organization, project, job);
return (
<TableRow
key={job.id}
className={cn(
(job.hasIntegrationsRequiringAction && "bg-rose-500/20") ||
(job.lastRun === undefined && "bg-green-500/20")
)}
>
<TableRow key={job.id} className="group">
<TableCell to={path}>
<span className="flex items-center gap-2">
<NamedIcon name={job.event.icon} className="h-8 w-8" />
@@ -145,13 +143,39 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul
<LabelValueStack label={"Never run"} value={""} />
)}
</TableCell>
<TableCellChevron to={path}>
{job.lastRun === undefined && (
<Badge className="mr-4" variant="green">
New Job!
</Badge>
)}
</TableCellChevron>
<TableCell to={path}>
<JobStatusBadge
enabled={job.status === "ACTIVE"}
hasIntegrationsRequiringAction={job.hasIntegrationsRequiringAction}
hasRuns={job.lastRun !== undefined}
/>
</TableCell>
<TableCellMenu isSticky>
<PopoverMenuItem to={path} title="View Job" icon="eye" />
<PopoverMenuItem
to={jobTestPath(organization, project, job)}
title="Test Job"
icon="beaker"
/>
<Dialog>
<DialogTrigger asChild>
<Button variant="menu-item" LeadingIcon="trash-can">
Delete Job
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DeleteJobDialogContent
id={job.id}
title={job.title}
slug={job.slug}
environments={job.environments}
/>
</DialogHeader>
</DialogContent>
</Dialog>
</TableCellMenu>
</TableRow>
);
})
@@ -11,7 +11,7 @@ import { docsRoot } from "~/utils/pathBuilder";
export function NavBar() {
return (
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-divide py-1 pl-1 pr-2.5">
<div className="z-50 flex w-full items-center justify-between gap-2 border-b border-uiBorder py-1 pl-1 pr-2.5">
<div className="flex gap-0.5">
<Link to="/" className="p-2">
<LogoIcon className="h-5 w-5" />
@@ -58,7 +58,7 @@ export function ProjectSideMenu() {
variants={menuVariants}
initial={isCollapsed ? "collapsed" : "expanded"}
className={cn(
"flex h-full flex-col justify-between overflow-hidden border-r border-slate-850 p-1 transition duration-300 ease-in-out"
"flex h-full flex-col justify-between overflow-hidden border-r border-uiBorder p-1 transition duration-300 ease-in-out"
)}
>
<div className="flex flex-col gap-1">
@@ -6,8 +6,6 @@ const variants = {
"grid place-items-center rounded-full px-2 h-5 tracking-wider text-xxs bg-slate-700 text-bright uppercase whitespace-nowrap",
outline:
"grid place-items-center rounded-sm px-1 h-5 tracking-wider text-xxs border border-dimmed text-dimmed uppercase whitespace-nowrap",
green:
"grid place-items-center rounded-sm px-1.5 h-5 tracking-wider outline-offset-1 outline outline-1 outline-green-600 text-xxs bg-green-500 text-slate-900 uppercase whitespace-nowrap",
};
type BadgeProps = React.HTMLAttributes<HTMLDivElement> & {
@@ -87,6 +87,14 @@ const variant = {
shortcutVariant: undefined,
shortcut: undefined,
},
"danger/large": {
textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50",
button:
"h-10 px-2 text-md bg-rose-600 group-hover:bg-rose-500 group-disabled:opacity-50 group-disabled:group-hover:bg-rose-600",
icon: "h-5",
shortcutVariant: "medium" as const,
shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60",
},
"menu-item": {
textColor: "text-bright",
button:
@@ -2,8 +2,9 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "~/utils/cn";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { ShortcutKey } from "./ShortcutKey";
const Dialog = DialogPrimitive.Root;
@@ -25,7 +26,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100",
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
className
)}
{...props}
@@ -42,15 +43,29 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in sm:max-w-lg sm:rounded-lg sm:zoom-in-90",
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-midnight-800 p-6 pt-11 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
className
)}
{...props}
>
<hr className="-ml-6 w-[calc(100%_+_3rem)]" />
{children}
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
<DialogPrimitive.Close className="absolute right-3 top-3 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none">
<div className="flex gap-x-2">
<ShortcutKey
shortcut={{
windows: {
key: "esc",
},
mac: {
key: "esc",
},
}}
variant={"small"}
/>
<XMarkIcon className="h-5 w-5" />
<span className="sr-only">Close</span>
</div>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
@@ -21,7 +21,7 @@ export function FormTitle({
<div
className={cn(
"mb-4 flex flex-col gap-3 pb-4",
divide ? "border-b border-slate-800" : "",
divide ? "border-b border-uiBorder" : "",
className
)}
>
@@ -27,6 +27,7 @@ import {
Cog8ToothIcon,
CreditCardIcon,
EnvelopeIcon,
EyeIcon,
FingerPrintIcon,
FlagIcon,
FolderIcon,
@@ -41,6 +42,7 @@ import {
QrCodeIcon,
SquaresPlusIcon,
StarIcon,
TrashIcon,
UserCircleIcon,
UserGroupIcon,
UserIcon,
@@ -50,7 +52,7 @@ import {
XMarkIcon,
} from "@heroicons/react/24/solid";
import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons";
import { HourglassIcon } from "lucide-react";
import { ActivityIcon, HourglassIcon } from "lucide-react";
import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon";
import { ErrorIcon } from "~/assets/icons/ErrorIcon";
import { ScheduleIcon } from "~/assets/icons/ScheduleIcon";
@@ -100,6 +102,7 @@ const icons = {
folder: (className: string) => <FolderIcon className={cn("text-indigo-600", className)} />,
envelope: (className: string) => <EnvelopeIcon className={cn("text-cyan-500", className)} />,
environment: (className: string) => <KeyIcon className={cn("text-yellow-500", className)} />,
eye: (className: string) => <EyeIcon className={cn("text-blue-500", className)} />,
globe: (className: string) => <GlobeAltIcon className={cn("text-fuchsia-600", className)} />,
"hand-raised": (className: string) => (
<HandRaisedIcon className={cn("text-amber-400", className)} />
@@ -129,6 +132,7 @@ const icons = {
<PlusSmallIcon className={cn("text-green-600", className)} />
),
property: (className: string) => <Cog8ToothIcon className={cn("text-slate-600", className)} />,
pulse: (className: string) => <ActivityIcon className={cn("text-green-600", className)} />,
"qr-code": (className: string) => <QrCodeIcon className={cn("text-amber-400", className)} />,
refresh: (className: string) => <ArrowPathIcon className={cn("text-bright", className)} />,
sapling: (className: string) => <SaplingIcon className={cn("text-green-500", className)} />,
@@ -139,6 +143,7 @@ const icons = {
star: (className: string) => <StarIcon className={cn("text-yellow-500", className)} />,
stop: (className: string) => <StopIcon className={cn("text-rose-500", className)} />,
team: (className: string) => <UserGroupIcon className={cn("text-blue-500", className)} />,
"trash-can": (className: string) => <TrashIcon className={cn("text-rose-500", className)} />,
tree: (className: string) => <OneTreeIcon className={cn("text-green-500", className)} />,
trees: (className: string) => <TwoTreesIcon className={cn("text-green-500", className)} />,
trigger: (className: string) => <BoltIcon className={cn("text-orange-500", className)} />,
@@ -13,7 +13,7 @@ type WithChildren = {
export function PageHeader({ children, hideBorder }: WithChildren & { hideBorder?: boolean }) {
return (
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-slate-800 pb-4")}>
<div className={cn("mx-4 pt-4", hideBorder ? "" : "border-b border-uiBorder pb-4")}>
{children}
</div>
);
@@ -4,9 +4,8 @@ import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "~/utils/cn";
import { Paragraph } from "./Paragraph";
import { ChevronDownIcon } from "@heroicons/react/24/solid";
import { ChevronDownIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid";
import { LinkButton } from "./Buttons";
import { IconNames } from "./NamedIcon";
const Popover = PopoverPrimitive.Root;
@@ -91,6 +90,24 @@ function PopoverArrowTrigger({
);
}
function PopoverVerticalEllipseTrigger({
isOpen,
className,
...props
}: { isOpen?: boolean } & React.ComponentPropsWithoutRef<typeof PopoverTrigger>) {
return (
<PopoverTrigger
{...props}
className={cn(
"group flex items-center justify-end gap-1 rounded px-1.5 py-1.5 text-dimmed transition hover:bg-slate-750 hover:text-bright",
className
)}
>
<EllipsisVerticalIcon className={cn("h-5 w-5 transition group-hover:text-bright")} />
</PopoverTrigger>
);
}
export {
Popover,
PopoverTrigger,
@@ -98,4 +115,5 @@ export {
PopoverSectionHeader,
PopoverArrowTrigger,
PopoverMenuItem,
PopoverVerticalEllipseTrigger,
};
@@ -51,7 +51,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-y border-slate-800",
"fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-uiBorder",
{
variants: {
position: {
@@ -154,7 +154,7 @@ const SheetContent = React.forwardRef<
{...props}
>
<div className="grid max-h-full grid-rows-[2.75rem_1fr] overflow-hidden">
<div className="flex items-center gap-2 border-b border-slate-800 p-2">
<div className="flex items-center gap-2 border-b border-uiBorder p-2">
<SheetPrimitive.Close className="rounded-sm p-1 transition hover:bg-slate-800 disabled:pointer-events-none">
<NamedIcon name="close" className="h-4 w-4" />
<span className="sr-only">Close</span>
@@ -181,7 +181,7 @@ export const SheetBody = ({ className, ...props }: React.HTMLAttributes<HTMLDivE
export const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"mx-4 flex shrink-0 items-center gap-4 border-b border-slate-800 py-3.5",
"mx-4 flex shrink-0 items-center gap-4 border-b border-uiBorder py-3.5",
className
)}
{...props}
@@ -194,7 +194,7 @@ export const SheetFooter = ({
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("shrink-0", className)} {...props}>
<div className="mx-4 border-t border-slate-800 py-3">{children}</div>
<div className="mx-4 border-t border-uiBorder py-3">{children}</div>
</div>
);
+64 -17
View File
@@ -1,25 +1,27 @@
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import { ReactNode, forwardRef } from "react";
import { ReactNode, forwardRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Badge } from "./Badge";
import { Popover, PopoverContent, PopoverVerticalEllipseTrigger } from "./Popover";
type TableProps = {
containerClassName?: string;
className?: string;
children: ReactNode;
fullWidth?: boolean;
};
export const Table = forwardRef<HTMLTableElement, TableProps>(
({ className, containerClassName, children }, ref) => {
({ className, containerClassName, children, fullWidth }, ref) => {
return (
<div
className={cn(
"overflow-x-auto whitespace-nowrap rounded-md border border-slate-900 scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
containerClassName
"overflow-x-auto whitespace-nowrap rounded-md border border-uiBorder scrollbar-thin scrollbar-track-midnight-850 scrollbar-thumb-slate-700",
containerClassName,
fullWidth && "w-full"
)}
>
<table ref={ref} className={cn("w-full divide-y bg-midnight-850", className)}>
<table ref={ref} className={cn("w-full divide-y", className)}>
{children}
</table>
</div>
@@ -37,7 +39,7 @@ export const TableHeader = forwardRef<HTMLTableSectionElement, TableHeaderProps>
return (
<thead
ref={ref}
className={cn("rounded-t-md", "relative divide-y divide-slate-850", className)}
className={cn("rounded-t-md", "relative divide-y divide-uiBorder bg-slate-850", className)}
>
{children}
</thead>
@@ -53,7 +55,7 @@ type TableBodyProps = {
export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
({ className, children }, ref) => {
return (
<tbody ref={ref} className={cn("relative divide-y divide-slate-850", className)}>
<tbody ref={ref} className={cn("relative divide-y divide-uiBorder", className)}>
{children}
</tbody>
);
@@ -63,12 +65,13 @@ export const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(
type TableRowProps = {
className?: string;
children: ReactNode;
disabled?: boolean;
};
export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
({ className, children }, ref) => {
({ className, disabled, children }, ref) => {
return (
<tr ref={ref} className={cn("group w-full", className)}>
<tr ref={ref} className={cn(disabled && "opacity-50", "group w-full", className)}>
{children}
</tr>
);
@@ -103,7 +106,7 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
ref={ref}
scope="col"
className={cn(
"px-4 py-3 align-middle text-xs font-semibold uppercase text-slate-400",
"px-4 py-3 align-middle text-xs font-normal uppercase tracking-wider text-dimmed",
alignmentClassName,
className
)}
@@ -118,10 +121,11 @@ export const TableHeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellP
type TableCellProps = TableCellBasicProps & {
to?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
hasAction?: boolean;
};
export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
({ className, alignment = "left", children, colSpan, to, onClick }, ref) => {
({ className, alignment = "left", children, colSpan, to, onClick, hasAction = false }, ref) => {
let alignmentClassName = "text-left";
switch (alignment) {
case "center":
@@ -133,7 +137,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
}
const flexClasses = cn(
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-slate-400",
"flex w-full whitespace-nowrap px-4 py-3 text-xs text-dimmed",
alignment === "left"
? "justify-start text-left"
: alignment === "center"
@@ -145,8 +149,10 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
<td
ref={ref}
className={cn(
"text-xs text-slate-400 transition group-hover:bg-slate-850/50",
to || onClick ? "cursor-pointer" : "px-4 py-3 align-middle",
"text-xs text-slate-400",
to || onClick || hasAction
? "cursor-pointer group-hover:bg-slate-900"
: "px-4 py-3 align-middle",
!to && !onClick && alignmentClassName,
className
)}
@@ -168,23 +174,64 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
}
);
const stickyStyles =
"sticky right-0 z-10 w-[2.8rem] min-w-[2.8rem] bg-background before:absolute before:pointer-events-none before:-left-8 before:top-0 before:h-full before:min-w-[2rem] before:bg-gradient-to-r before:from-transparent before:to-background before:content-[''] group-hover:before:to-slate-900";
export const TableCellChevron = forwardRef<
HTMLTableCellElement,
{
className?: string;
to?: string;
children?: ReactNode;
isSticky?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
>(({ className, to, children, onClick }, ref) => {
>(({ className, to, children, isSticky, onClick }, ref) => {
return (
<TableCell className={className} to={to} onClick={onClick} ref={ref} alignment="right">
<TableCell
className={cn(isSticky && stickyStyles, className)}
to={to}
onClick={onClick}
ref={ref}
alignment="right"
>
{children}
<ChevronRightIcon className="h-4 w-4 text-dimmed transition group-hover:text-bright" />
</TableCell>
);
});
export const TableCellMenu = forwardRef<
HTMLTableCellElement,
{
className?: string;
children?: ReactNode;
isSticky?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
>(({ className, children, isSticky, onClick }, ref) => {
const [isOpen, setIsOpen] = useState(false);
return (
<TableCell
className={cn(isSticky && stickyStyles, className)}
onClick={onClick}
ref={ref}
alignment="right"
hasAction={true}
>
<Popover onOpenChange={(open) => setIsOpen(open)}>
<PopoverVerticalEllipseTrigger isOpen={isOpen} />
<PopoverContent
className="w-fit max-w-[10rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
align="end"
>
<div className="flex flex-col gap-1 p-1">{children}</div>
</PopoverContent>
</Popover>
</TableCell>
);
});
type TableBlankRowProps = {
className?: string;
colSpan: number;
@@ -12,7 +12,7 @@ export type TabsProps = {
export function Tabs({ tabs, className }: TabsProps) {
return (
<div className={cn(`flex flex-row gap-x-6 border-b border-slate-800`, className)}>
<div className={cn(`flex flex-row gap-x-6 border-b border-uiBorder`, className)}>
{tabs.map((tab, index) => (
<NavLink key={index} to={tab.to} className="group flex flex-col items-center pt-1" end>
{({ isActive, isPending }) => (
@@ -109,7 +109,7 @@ export function RunsTable({
<TableCell to={path}>
{run.createdAt ? <DateTime date={run.createdAt} /> : ""}
</TableCell>
<TableCellChevron to={path} />
<TableCellChevron to={path} isSticky />
</TableRow>
);
})
@@ -20,7 +20,6 @@ function BadgesExample() {
<div className="flex flex-col items-start gap-y-8 p-8">
<Badge>Default</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="green">Green</Badge>
</div>
);
}
@@ -253,13 +253,11 @@ function ButtonList({ primary }: { primary: string }) {
<Header1 className="mb-2 mt-8">Large buttons</Header1>
<div className="grid grid-cols-1 gap-8 border-b border-slate-700 pb-8">
<div className="flex flex-col gap-2">
<div className="flex flex-col items-start">
<div className="flex flex-col items-start gap-2">
<Button variant="primary/large" fullWidth>
<NamedIcon name={"github"} className={"mr-1.5 h-4 w-4"} />
Continue with GitHub
</Button>
</div>
<div className="flex flex-col items-start">
<Button variant="secondary/large" fullWidth>
<NamedIcon
name={"envelope"}
@@ -267,6 +265,13 @@ function ButtonList({ primary }: { primary: string }) {
/>
Continue with Email
</Button>
<Button variant="danger/large" fullWidth>
<NamedIcon
name={"trash-can"}
className={"mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"}
/>
This is a delete button
</Button>
</div>
</div>
</div>
+6
View File
@@ -30,6 +30,8 @@ export type PrismaTransactionOptions = {
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
isolationLevel?: Prisma.TransactionIsolationLevel;
rethrowPrismaErrors?: boolean;
};
export async function $transaction<R>(
@@ -53,6 +55,10 @@ export async function $transaction<R>(
name: error.name,
});
if (options?.rethrowPrismaErrors) {
throw error;
}
return;
}
+4
View File
@@ -8,6 +8,10 @@ export type ProjectJob = UseDataFunctionReturn<typeof loader>["projectJobs"][num
export const jobsMatchId =
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
// 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[]) {
const routeMatch = useTypedMatchesData<typeof loader>({
id: jobsMatchId,
+19
View File
@@ -102,6 +102,25 @@ export async function jsonWithSuccessMessage(
});
}
export async function jsonWithErrorMessage(
data: any,
request: Request,
message: string,
options?: ToastMessageOptions
) {
const session = await getSession(request.headers.get("cookie"));
setErrorMessage(session, message, options);
return json(data, {
headers: {
"Set-Cookie": await commitSession(session, {
expires: new Date(Date.now() + ONE_YEAR),
}),
},
});
}
export async function redirectWithSuccessMessage(
path: string,
request: Request,
@@ -49,6 +49,7 @@ export function getOrganizations({ userId }: { userId: User["id"] }) {
jobs: {
where: {
internal: false,
deletedAt: null,
},
},
},
+6 -2
View File
@@ -108,6 +108,10 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#recurringTasks = options.recurringTasks;
}
get graphileWorkerSchema() {
return this.#runnerOptions.schema ?? "graphile_worker";
}
public async initialize(): Promise<boolean> {
if (this.#runner) {
return true;
@@ -252,7 +256,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
tx: PrismaClientOrTransaction
) {
const results = await tx.$queryRawUnsafe(
`SELECT * FROM graphile_worker.add_job(
`SELECT * FROM ${this.graphileWorkerSchema}.add_job(
identifier => $1::text,
payload => $2::json,
queue_name => $3::text,
@@ -290,7 +294,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
async #removeJob(jobKey: string, tx: PrismaClientOrTransaction) {
try {
const result = await tx.$queryRawUnsafe(
`SELECT * FROM graphile_worker.remove_job(
`SELECT * FROM ${this.graphileWorkerSchema}.remove_job(
job_key => $1::text
)`,
jobKey
@@ -63,6 +63,7 @@ export class IntegrationClientPresenter {
slug: projectSlug,
},
internal: false,
deletedAt: null,
},
},
},
@@ -71,6 +71,7 @@ export class IntegrationsPresenter {
slug: projectSlug,
},
internal: false,
deletedAt: null,
},
},
},
@@ -47,6 +47,7 @@ export class JobListPresenter {
version: true,
eventSpecification: true,
properties: true,
status: true,
runs: {
select: {
createdAt: true,
@@ -92,6 +93,7 @@ export class JobListPresenter {
},
where: {
internal: false,
deletedAt: null,
organization: orgWhere,
project: {
slug: projectSlug,
@@ -162,11 +164,19 @@ export class JobListPresenter {
properties = [...properties, ...versionProperties];
}
const environments = job.aliases.map((alias) => ({
type: alias.environment.type,
enabled: alias.version.status === "ACTIVE",
lastRun: alias.version.runs.at(0)?.createdAt,
version: alias.version.version,
}));
return {
id: job.id,
slug: job.slug,
title: job.title,
version: alias.version.version,
status: alias.version.status,
dynamic: job.dynamicTriggers.length > 0,
event: {
title: eventSpecification.title,
@@ -179,6 +189,7 @@ export class JobListPresenter {
),
lastRun,
properties,
environments,
};
})
.filter(Boolean);
@@ -80,6 +80,7 @@ export class ProjectPresenter {
},
where: {
internal: false,
deletedAt: null,
},
orderBy: [{ title: "asc" }],
},
@@ -73,8 +73,6 @@ export default function Page() {
const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs);
const { width, height } = useWindowSize();
return (
<PageContainer>
<PageHeader>
@@ -104,7 +102,6 @@ export default function Page() {
</Callout>
)}
<div className="mb-2 flex flex-col">
<Header2 spacing>Jobs</Header2>
<div className="flex w-full">
<Input
placeholder="Search Jobs"
@@ -122,9 +119,11 @@ export default function Page() {
noResultsText={`No Jobs match ${filterText}. Try a different search
query.`}
/>
{jobs.length === 1 && jobs.every((r) => r.lastRun === undefined) && (
<RunYourJobPrompt />
)}
{jobs.length === 1 &&
jobs.every((r) => r.lastRun === undefined) &&
jobs.every((i) => i.hasIntegrationsRequiringAction === false) && (
<RunYourJobPrompt />
)}
</>
) : (
<HowToSetupYourProject />
@@ -194,7 +193,7 @@ function ExampleJobs() {
<a
href={example.codeLink}
key={example.title}
className="flex w-full items-center rounded border-b border-slate-800 py-2 transition hover:border-transparent hover:bg-slate-800"
className="flex w-full items-center rounded border-b border-uiBorder py-2 transition hover:border-transparent hover:bg-slate-800"
>
{example.icon}
<Paragraph variant="small">
@@ -355,7 +355,7 @@ function ConnectedIntegrationsList({
<TableCell to={path}>
<DateTime date={client.createdAt} includeSeconds={false} />
</TableCell>
<TableCellChevron to={path} />
<TableCellChevron to={path} isSticky />
</TableRow>
);
})}
@@ -454,7 +454,7 @@ function IntegrationsWithMissingFields({
integration={integration}
organizationId={organizationId}
button={
<ChevronRightIcon className="h-4 w-4 text-slate-700 transition group-hover:text-bright" />
<ChevronRightIcon className="h-4 w-4 text-dimmed transition group-hover:text-bright" />
}
callbackUrl={callbackUrl}
existingIntegration={client}
@@ -482,7 +482,7 @@ function AddIntegrationConnection({
icon?: string;
}) {
return (
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-850">
<div className="group flex h-11 w-full items-center gap-2 rounded-md p-1 pr-3 transition hover:bg-slate-900">
<NamedIconInBox
name={icon ?? identifier}
className="h-9 w-9 flex-none transition group-hover:border-slate-750"
@@ -1,11 +1,68 @@
import { ComingSoon } from "~/components/ComingSoon";
import { JobEnvironment, JobStatusTable } from "~/components/JobsStatusTable";
import { HowToDisableAJob } from "~/components/helpContent/HelpContentText";
import { DeleteJobDialogContent } from "~/components/jobs/DeleteJobModalContent";
import { Button } from "~/components/primitives/Buttons";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
import { Header2 } from "~/components/primitives/Headers";
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
import { Paragraph } from "~/components/primitives/Paragraph";
import { useJob } from "~/hooks/useJob";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { projectJobsPath, projectPath } from "~/utils/pathBuilder";
export default function Page() {
const job = useJob();
const organization = useOrganization();
const project = useProject();
return (
<ComingSoon
title="Job settings"
description="Disable a Job, archive it and more."
icon="settings"
/>
<Help defaultOpen>
{(open) => (
<div className={cn("grid h-fit gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div className="w-full">
<div className="flex items-center justify-between">
<Header2 className="mb-2 flex items-center gap-1">Environments</Header2>
<HelpTrigger title="How do disable a Job?" />
</div>
<JobStatusTable environments={job.environments} />
<div className="mt-4 flex w-full items-center justify-end gap-x-3">
{job.status === "ACTIVE" && (
<Paragraph variant="small">
Disable this Job in all environments before deleting
</Paragraph>
)}
<Dialog>
<DialogTrigger asChild>
<Button
variant="danger/small"
leadingIconClassName="text-bright"
LeadingIcon="trash-can"
>
I want to delete this Job
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DeleteJobDialogContent
title={job.title}
slug={job.slug}
environments={job.environments}
id={job.id}
redirectTo={projectJobsPath(organization, project)}
/>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
</div>
<HelpContent title="How to disable a Job">
<HowToDisableAJob id={job.slug} version={job.version} name={job.title} />
</HelpContent>
</div>
)}
</Help>
);
}
@@ -2,6 +2,7 @@ import { Outlet, useLocation } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { Fragment } from "react";
import { typedjson } from "remix-typedjson";
import { JobStatusBadge } from "~/components/jobs/JobStatusBadge";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { JobsMenu } from "~/components/navigation/JobsMenu";
import { BreadcrumbLink } from "~/components/navigation/NavBar";
@@ -134,6 +135,18 @@ export default function Job() {
}
/>
)}
<PageInfoProperty
icon="pulse"
label={"STATUS"}
value={
<JobStatusBadge
enabled={job.status === "ACTIVE"}
hasIntegrationsRequiringAction={job.hasIntegrationsRequiringAction}
hasRuns={job.lastRun !== undefined}
badgeSize="small"
/>
}
/>
</PageInfoGroup>
<PageInfoGroup alignment="right">
<Paragraph variant="extra-small" className="text-slate-600">
@@ -103,7 +103,7 @@ export default function Page() {
<OrgAdminHeader />
<PageBody>
<Header2>Members</Header2>
<ul className="flex w-full max-w-md flex-col divide-y divide-slate-800 border-b border-slate-800">
<ul className="flex w-full max-w-md flex-col divide-y divide-uiBorder border-b border-uiBorder">
{members.map((member) => (
<li key={member.user.id} className="flex items-center gap-x-4 py-4">
<UserAvatar
@@ -0,0 +1,52 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { eventRecordToApiJson } from "~/api.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CancelEventService } from "~/services/events/cancelEvent.server";
import { logger } from "~/services/logger.server";
const ParamsSchema = z.object({
eventId: z.string(),
});
export async function action({ request, params }: ActionArgs) {
// 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 CancelEventService();
try {
const updatedEvent = await service.call(authenticatedEnv, eventId);
if (!updatedEvent) {
return json({ error: "Event not found" }, { status: 404 });
}
return json(eventRecordToApiJson(updatedEvent));
} catch (err) {
logger.error("CancelEventService.call() error", {
error: err,
});
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
@@ -1,6 +1,6 @@
import type { ActionArgs, LoaderArgs } from "@remix-run/server-runtime";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { cors } from "remix-utils";
import { GetEvent } from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -32,9 +32,36 @@ export async function loader({ request, params }: LoaderArgs) {
const { eventId } = parsed.data;
const event = await prisma.eventRecord.findFirst({
const event = await findEventRecord(eventId, authenticatedEnv.id);
if (!event) {
return apiCors(request, json({ error: "Event not found" }, { status: 404 }));
}
return apiCors(request, json(toJSON(event)));
}
function toJSON(eventRecord: FoundEventRecord): GetEvent {
return {
id: eventRecord.eventId,
name: eventRecord.name,
createdAt: eventRecord.createdAt,
updatedAt: eventRecord.updatedAt,
runs: eventRecord.runs.map((run) => ({
id: run.id,
status: run.status,
startedAt: run.startedAt,
completedAt: run.completedAt,
})),
};
}
type FoundEventRecord = NonNullable<Awaited<ReturnType<typeof findEventRecord>>>;
async function findEventRecord(eventId: string, environmentId: string) {
return await prisma.eventRecord.findUnique({
select: {
id: true,
eventId: true,
name: true,
createdAt: true,
updatedAt: true,
@@ -48,14 +75,10 @@ export async function loader({ request, params }: LoaderArgs) {
},
},
where: {
id: eventId,
environmentId: authenticatedEnv.id,
eventId_environmentId: {
eventId,
environmentId,
},
},
});
if (!event) {
return apiCors(request, json({ error: "Event not found" }, { status: 404 }));
}
return apiCors(request, json(event));
}
+6 -1
View File
@@ -4,6 +4,7 @@ import { SendEventBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { IngestSendEvent } from "~/services/events/ingestSendEvent.server";
import { eventRecordToApiJson } from "~/api.server";
export async function action({ request }: ActionArgs) {
// Ensure this is a POST request
@@ -33,5 +34,9 @@ export async function action({ request }: ActionArgs) {
const event = await service.call(authenticatedEnv, body.data.event, body.data.options);
return json(event);
if (!event) {
return json({ error: "Failed to create event" }, { status: 500 });
}
return json(eventRecordToApiJson(event));
}
@@ -0,0 +1,74 @@
import { ActionFunction } from "@remix-run/node";
import { z } from "zod";
import { prisma } from "~/db.server";
import {
jsonWithErrorMessage,
jsonWithSuccessMessage,
redirectWithSuccessMessage,
} from "~/models/message.server";
import { DeleteJobService } from "~/services/jobs/deleteJob.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
const ParamSchema = z.object({
jobId: z.string(),
});
export const action: ActionFunction = async ({ request, params }) => {
const { jobId } = ParamSchema.parse(params);
const userId = await requireUserId(request);
// Find the job
const job = await prisma.job.findFirst({
where: {
id: jobId,
organization: {
members: {
some: {
userId,
},
},
},
},
});
if (!job) {
return jsonWithErrorMessage({ ok: false }, request, `Job could not be scheduled for deletion.`);
}
try {
const deleteJobService = new DeleteJobService();
await deleteJobService.call(job);
const url = new URL(request.url);
const redirectTo = url.searchParams.get("redirectTo");
logger.debug("Job scheduled for deletion", {
url,
redirectTo,
job,
});
if (typeof redirectTo === "string" && redirectTo.length > 0) {
return redirectWithSuccessMessage(
redirectTo,
request,
`Job ${job.slug} has been scheduled for deletion.`
);
}
return jsonWithSuccessMessage(
{ ok: true },
request,
`Job ${job.slug} has been scheduled for deletion.`
);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return jsonWithErrorMessage(
{ ok: false },
request,
`Job could not be scheduled for deletion: ${message}`
);
}
};
@@ -7,10 +7,12 @@ import { logger } from "../logger.server";
import { RegisterSourceService } from "../sources/registerSource.server";
import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
import { DisableJobService } from "../jobs/disableJob.server";
export class IndexEndpointService {
#prismaClient: PrismaClient;
#registerJobService = new RegisterJobService();
#disableJobService = new DisableJobService();
#registerSourceService = new RegisterSourceService();
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
@@ -57,23 +59,98 @@ export class IndexEndpointService {
sources: 0,
dynamicTriggers: 0,
dynamicSchedules: 0,
disabledJobs: 0,
};
const existingJobs = await this.#prismaClient.job.findMany({
where: {
projectId: endpoint.projectId,
deletedAt: null,
},
include: {
aliases: {
where: {
name: "latest",
environmentId: endpoint.environmentId,
},
include: {
version: true,
},
take: 1,
},
},
});
for (const job of jobs) {
if (!job.enabled) {
continue;
const disabledJob = await this.#disableJobService
.call(endpoint, { slug: job.id, version: job.version })
.catch((error) => {
logger.error("Failed to disable job", {
endpointId: endpoint.id,
job,
error,
});
return;
});
if (disabledJob) {
indexStats.disabledJobs++;
}
} else {
try {
const registeredVersion = await this.#registerJobService.call(endpoint, job);
if (registeredVersion) {
indexStats.jobs++;
}
} catch (error) {
logger.error("Failed to register job", {
endpointId: endpoint.id,
job,
error,
});
}
}
}
try {
await this.#registerJobService.call(endpoint, job);
// TODO: we need to do this for sources, dynamic triggers, and dynamic schedules
const missingJobs = existingJobs.filter((job) => {
return !jobs.find((j) => j.id === job.slug);
});
indexStats.jobs++;
} catch (error) {
logger.error("Failed to register job", {
endpointId: endpoint.id,
job,
error,
});
if (missingJobs.length > 0) {
logger.debug("Disabling missing jobs", {
endpointId: endpoint.id,
missingJobIds: missingJobs.map((job) => job.slug),
});
for (const job of missingJobs) {
const latestVersion = job.aliases[0]?.version;
if (!latestVersion) {
continue;
}
const disabledJob = await this.#disableJobService
.call(endpoint, {
slug: job.slug,
version: latestVersion.version,
})
.catch((error) => {
logger.error("Failed to disable job", {
endpointId: endpoint.id,
job,
error,
});
return;
});
if (disabledJob) {
indexStats.disabledJobs++;
}
}
}
@@ -0,0 +1,47 @@
import type { EventRecord } from "@trigger.dev/database";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
export class CancelEventService {
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
eventId: string
): Promise<EventRecord | undefined> {
return await $transaction(this.#prismaClient, async (tx) => {
const event = await tx.eventRecord.findUnique({
where: {
eventId_environmentId: {
eventId: eventId,
environmentId: environment.id,
},
},
});
if (!event) {
return;
}
if (event.cancelledAt) {
return event;
}
//update the cancelledAt column in the eventRecord table
const updatedEvent = await tx.eventRecord.update({
where: { id: event.id },
data: { cancelledAt: new Date() },
});
// Dequeue the event after the db has been updated
await workerQueue.dequeue(`event:${event.id}`, { tx });
return updatedEvent;
});
}
}
@@ -2,14 +2,12 @@ import type { RawEvent, SendEventOptions } from "@trigger.dev/core";
import { $transaction, PrismaClientOrTransaction, PrismaErrorSchema, prisma } from "~/db.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { workerQueue } from "~/services/worker.server";
import { logger } from "../logger.server";
export class IngestSendEvent {
#prismaClient: PrismaClientOrTransaction;
constructor(
prismaClient: PrismaClientOrTransaction = prisma,
private deliverEvents = true
) {
constructor(prismaClient: PrismaClientOrTransaction = prisma, private deliverEvents = true) {
this.#prismaClient = prismaClient;
}
@@ -36,71 +34,87 @@ export class IngestSendEvent {
try {
const deliverAt = this.#calculateDeliverAt(options);
return await $transaction(this.#prismaClient, async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.findUniqueOrThrow({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: options.accountId,
return await $transaction(
this.#prismaClient,
async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.findUniqueOrThrow({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: options.accountId,
},
},
})
: undefined;
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organization: {
connect: {
id: environment.organizationId,
},
},
})
: undefined;
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organization: {
connect: {
id: environment.organizationId,
project: {
connect: {
id: environment.projectId,
},
},
},
project: {
connect: {
id: environment.projectId,
environment: {
connect: {
id: environment.id,
},
},
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccount: externalAccount
? {
connect: {
id: externalAccount.id,
},
}
: {},
},
environment: {
connect: {
id: environment.id,
});
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
},
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccount: externalAccount
? {
connect: {
id: externalAccount.id,
},
}
: {},
},
});
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
);
}
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
{ runAt: eventLog.deliverAt, tx }
);
}
return eventLog;
});
return eventLog;
},
{ rethrowPrismaErrors: true }
);
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
if (!prismaError.success) {
logger.debug("Error parsing prisma error", {
error,
parseError: prismaError.error.format(),
});
throw error;
}
// If the error is a Prisma unique constraint error, it means that the event already exists
if (prismaError.success && prismaError.data.code === "P2002") {
logger.debug("Event already exists, finding and returning", { event, environment });
return this.#prismaClient.eventRecord.findUniqueOrThrow({
where: {
eventId_environmentId: {
@@ -0,0 +1,45 @@
import type { Job } from "@trigger.dev/database";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { telemetry } from "../telemetry.server";
export class DeleteJobService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(job: Job) {
// Make sure that all the latest versions are disabled
const latestVersions = await this.#prismaClient.jobAlias.findMany({
where: {
jobId: job.id,
name: "latest",
},
include: {
version: true,
},
});
const allDisabled = latestVersions.every((alias) => alias.version.status === "DISABLED");
if (!allDisabled) {
throw new Error("All latest versions must be disabled before deleting a job");
}
// Okay now we need to delete a job by setting the deletedAt field and enqueuing a job to cleanup the job
await this.#prismaClient.job.update({
where: {
id: job.id,
},
data: {
deletedAt: new Date(),
},
});
telemetry.project.deletedJob({
job,
});
}
}
@@ -0,0 +1,103 @@
import type { JobVersion } from "@trigger.dev/database";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
import type { AuthenticatedEnvironment } from "../apiAuth.server";
import { DisableScheduleSourceService } from "../schedules/disableScheduleSource.server";
export type DisableJobServiceOptions = {
slug: string;
version: string;
};
export class DisableJobService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
endpointIdOrEndpoint: string | ExtendedEndpoint,
options: DisableJobServiceOptions
) {
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
return this.#disableJob(endpoint.environment, options);
}
async #disableJob(
environment: AuthenticatedEnvironment,
options: DisableJobServiceOptions
): Promise<JobVersion | undefined> {
// Find the job
const job = await this.#prismaClient.job.findUnique({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: options.slug,
},
},
});
if (!job) {
return;
}
const jobVersion = await this.#prismaClient.jobVersion.findUnique({
where: {
jobId_version_environmentId: {
jobId: job.id,
version: options.version,
environmentId: environment.id,
},
},
});
if (!jobVersion) {
return;
}
if (jobVersion.status === "DISABLED") {
return;
}
// Upsert the JobVersion
const updatedJobVersion = await this.#prismaClient.jobVersion.update({
where: {
id: jobVersion.id,
},
data: {
status: "DISABLED",
},
});
await this.#disableEventDispatcher(updatedJobVersion);
return updatedJobVersion;
}
async #disableEventDispatcher(jobVersion: JobVersion) {
const eventDispatcher = await this.#prismaClient.eventDispatcher.update({
where: {
dispatchableId_environmentId: {
dispatchableId: jobVersion.jobId,
environmentId: jobVersion.environmentId,
},
},
data: {
enabled: false,
},
});
const service = new DisableScheduleSourceService();
await service.call({
key: jobVersion.jobId,
dispatcher: eventDispatcher,
});
}
}
@@ -34,7 +34,21 @@ export class RegisterJobService {
endpoint: Endpoint,
environment: AuthenticatedEnvironment,
metadata: JobMetadata
): Promise<JobVersion> {
): Promise<JobVersion | undefined> {
// Check the job doesn't already exist and is deleted
const existingJob = await this.#prismaClient.job.findUnique({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: metadata.id,
},
},
});
if (existingJob && existingJob.deletedAt && !metadata.enabled) {
return;
}
const integrations = new Map<string, Integration>();
for (const [, jobIntegration] of Object.entries(metadata.integrations)) {
@@ -155,6 +169,7 @@ export class RegisterJobService {
},
update: {
title: metadata.name,
deletedAt: metadata.enabled ? null : undefined,
},
include: {
integrations: {
@@ -236,8 +251,10 @@ export class RegisterJobService {
eventSpecification,
preprocessRuns: metadata.preprocessRuns,
startPosition: "LATEST",
status: "ACTIVE",
},
update: {
status: "ACTIVE",
startPosition: "LATEST",
eventSpecification,
preprocessRuns: metadata.preprocessRuns,
@@ -401,6 +418,7 @@ export class RegisterJobService {
type: "JOB_VERSION",
id: jobVersion.id,
},
enabled: true,
},
});
@@ -444,6 +462,7 @@ export class RegisterJobService {
type: "JOB_VERSION",
id: jobVersion.id,
},
enabled: true,
},
});
@@ -1,5 +1,4 @@
import {
ApiEventLogSchema,
CachedTaskSchema,
RunJobError,
RunJobResumeWithTask,
@@ -9,6 +8,7 @@ import {
} from "@trigger.dev/core";
import type { Task } from "@trigger.dev/database";
import { generateErrorMessage } from "zod-error";
import { eventRecordToApiJson } from "~/api.server";
import { EXECUTE_JOB_RETRY_LIMIT } from "~/consts";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { enqueueRunExecutionV1 } from "~/models/jobRunExecution.server";
@@ -54,7 +54,7 @@ export class PerformRunExecutionV1Service {
const { run } = execution;
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const event = eventRecordToApiJson(run.event);
const startedAt = new Date();
await this.#prismaClient.jobRunExecution.update({
@@ -174,7 +174,7 @@ export class PerformRunExecutionV1Service {
}
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const event = eventRecordToApiJson(run.event);
const startedAt = new Date();
@@ -1,5 +1,4 @@
import {
ApiEventLogSchema,
CachedTask,
RunJobError,
RunJobResumeWithTask,
@@ -9,6 +8,7 @@ import {
} from "@trigger.dev/core";
import type { Task } from "@trigger.dev/database";
import { generateErrorMessage } from "zod-error";
import { eventRecordToApiJson } from "~/api.server";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
import { resolveRunConnections } from "~/models/runConnection.server";
@@ -57,7 +57,7 @@ export class PerformRunExecutionV2Service {
// the run execution will be marked as failed and the run will start
async #executePreprocessing(run: FoundRun) {
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const event = eventRecordToApiJson(run.event);
const { response, parser } = await client.preprocessRunRequest({
event,
@@ -146,7 +146,7 @@ export class PerformRunExecutionV2Service {
}
const client = new EndpointApi(run.environment.apiKey, run.endpoint.url);
const event = ApiEventLogSchema.parse({ ...run.event, id: run.eventId });
const event = eventRecordToApiJson(run.event);
const startedAt = new Date();
@@ -0,0 +1,44 @@
import type { EventDispatcher } from "@trigger.dev/database";
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
export class DisableScheduleSourceService {
#prismaClient: PrismaClientOrTransaction;
constructor(prismaClient: PrismaClientOrTransaction = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ key, dispatcher }: { key: string; dispatcher: EventDispatcher }) {
const scheduleSourceExists = await this.#prismaClient.scheduleSource.findUnique({
where: {
key_environmentId: {
key,
environmentId: dispatcher.environmentId,
},
},
});
if (!scheduleSourceExists) {
return;
}
return await $transaction(this.#prismaClient, async (tx) => {
const scheduleSource = await this.#prismaClient.scheduleSource.update({
where: {
key_environmentId: {
key,
environmentId: dispatcher.environmentId,
},
},
data: {
active: false,
},
});
await workerQueue.dequeue(`scheduled:${scheduleSource.id}`, { tx });
return scheduleSource;
});
}
}
@@ -56,7 +56,6 @@ export class NextScheduledEventService {
},
{
runAt: scheduleTime,
queueName: `scheduler:${scheduleSource.environmentId}`,
tx,
jobKey: `scheduled:${scheduleSource.id}`,
}
@@ -70,6 +70,7 @@ export class RegisterScheduleSourceService {
},
metadata: schedule.metadata ?? {},
externalAccountId: externalAccount ? externalAccount.id : undefined,
active: environment.autoEnableInternalSources,
},
});
@@ -1,8 +1,5 @@
import { RegisterScheduleBody } from "@trigger.dev/core";
import { $transaction, PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { PrismaClient, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { RegisterScheduleSourceService } from "./registerScheduleSource.server";
export class UnregisterScheduleService {
#prismaClient: PrismaClient;
@@ -1,3 +1,4 @@
import { Job } from "@trigger.dev/database";
import { TriggerClient } from "@trigger.dev/sdk";
import { PostHog } from "posthog-node";
import { env } from "~/env.server";
@@ -149,6 +150,14 @@ class Telemetry {
},
});
},
deletedJob: ({ job }: { job: Job }) => {
this.#triggerClient?.sendEvent({
name: "job.deleted",
payload: {
id: job.id,
},
});
},
};
#capture(event: CaptureEvent) {
+4
View File
@@ -115,6 +115,10 @@ export function projectPath(organization: OrgForPath, project: ProjectForPath) {
return `/orgs/${organizationParam(organization)}/projects/${projectParam(project)}`;
}
export function projectJobsPath(organization: OrgForPath, project: ProjectForPath) {
return projectPath(organization, project);
}
export function projectIntegrationsPath(organization: OrgForPath, project: ProjectForPath) {
return `${projectPath(organization, project)}/integrations`;
}
+1
View File
@@ -144,6 +144,7 @@ module.exports = {
},
devEnv: colors.pink,
liveEnv: colors.green,
uiBorder: slate[800],
},
borderRadius: {
lg: radius,
+70 -8
View File
@@ -60,15 +60,13 @@ yarn dlx @trigger.dev/cli@latest init
<Accordion title="Enter your development API key">
To locate your development API key, login to the [Trigger.dev
dashboard](https://cloud.trigger.dev) and select the Project you want to
connect to. Then click on the Environments & API Keys tab in the left menu.
You can copy your development API Key from the field at the top of this page.
(Your development key will start with `tr_dev_`).
dashboard](https://cloud.trigger.dev) and select the Project you want to connect to. Then click on
the Environments & API Keys tab in the left menu. You can copy your development API Key from the
field at the top of this page. (Your development key will start with `tr_dev_`).
</Accordion>
<Accordion title="Enter a unique ID for your endpoint">
Enter a custom ID or use the default by hitting enter. You can learn more
about endpoints
Enter a custom ID or use the default by hitting enter. You can learn more about endpoints
[here](/documentation/concepts/environments-endpoints#endpoints).
</Accordion>
@@ -81,8 +79,8 @@ Once you're running your Next.js project locally, you can then execute the `dev`
![Your first Job](/images/cli-dev.gif)
<Warning>
Make sure your Next.js site is running locally before continuing. You must
also leave this `dev` terminal command running while you develop.
Make sure your Next.js site is running locally before continuing. You must also leave this `dev`
terminal command running while you develop.
</Warning>
In a **new terminal window or tab** run:
@@ -107,3 +105,67 @@ yarn dlx @trigger.dev/cli@latest dev
You can optionally pass the port if you're not running on 3000 by adding
`--port 3001` to the end
</Note>
<Note>
You can optionally pass the hostname if you're not running on localhost by adding
`--hostname <host>`. Example, in case your Next.js is running on 0.0.0.0: `--hostname 0.0.0.0`.
</Note>
## update Command
The `update` command will update all Trigger.dev packages to the latest version.
<CodeGroup>
```bash npm
npx @trigger.dev/cli@latest update
```
```bash pnpm
pnpm dlx @trigger.dev/cli@latest update
```
```bash yarn
yarn dlx @trigger.dev/cli@latest update
```
</CodeGroup>
## whoami Command
The `whoami` command will print out information about your current Trigger.dev project and environment, based on the API key found in your `.env` or `.env.local` file
<CodeGroup>
```bash npm
npx @trigger.dev/cli@latest whoami
```
```bash pnpm
pnpm dlx @trigger.dev/cli@latest whoami
```
```bash yarn
yarn dlx @trigger.dev/cli@latest whoami
```
</CodeGroup>
## send-event Command
The `send-event` command will send an event to your Trigger.dev project. This is useful for testing your Trigger.dev project locally.
<CodeGroup>
```bash npm
npx @trigger.dev/cli@latest send-event -n "event.name" -p "{ \"key\": \"value\" }"
```
```bash pnpm
pnpm dlx @trigger.dev/cli@latest send-event -n "event.name" -p "{ \"key\": \"value\" }"
```
```bash yarn
yarn dlx @trigger.dev/cli@latest send-event -n "event.name" -p "{ \"key\": \"value\" }"
```
</CodeGroup>
@@ -0,0 +1,84 @@
---
title: "Managing Jobs"
description: "Managing jobs in your codebase and the dashboard"
---
## Disabling jobs
To prevent a job from processing new runs, you can disable it by setting the `enabled` option:
```ts
client.defineJob({
id: "example-job",
name: "Example Job",
version: "0.1.0",
trigger: eventTrigger({ name: "example.event" }),
enabled: false,
run: async (payload, io, ctx) => {
// your job code here
},
});
```
If you omit the `enabled` option, it will default to `true`.
The job will only be disabled in environments that have seen the `enabled = false` value. So the job will remain enabled in production until the code with the `enabled = false` is deployed to production.
<Note>
Currently this is the only way to disable a job. If you'd like to disable a job in the Dashboard,
please reach out to us on [Discord](https://discord.gg/kA47vcd8P6) and let us know 👋
</Note>
Once a job is disabled no **new** runs will be created for that job, and it will still be visible in the Dashboard as disabled:
![Disabled Job](/images/disabled-job.png)
### In-progress runs
In-progress runs will be allowed to finish, even runs that are currently delayed from a call to `io.wait`. If you'd like to completely stop in-progress runs, you have two options:
- Set the `enabled` option to false and then `throw` an error at the top of your job `run` function.
```ts
client.defineJob({
id: "example-job",
name: "Example Job",
version: "0.1.0",
trigger: eventTrigger({ name: "example.event" }),
enabled: false,
run: async (payload, io, ctx) => {
throw new Error("Job disabled");
},
});
```
- Delete the job from your codebase. This will disable the job as well but also stop in progress runs.
### Disabling in production with env vars
You can easily disable jobs in production using env vars so you don't have to deploy new code to disable a job.
```ts
client.defineJob({
id: "example-job",
name: "Example Job",
version: "0.1.0",
trigger: eventTrigger({ name: "example.event" }),
enabled: process.env.TRIGGER_JOBS_DISABLED === "true",
run: async (payload, io, ctx) => {
// your job code here
},
});
```
Then you can disable the job in production by setting the `TRIGGER_JOBS_DISABLED` env var to `"true"`. And removing the env var will re-enable the job.
## Deleting jobs
Once you have disabled a job in all environments, you can delete it from the dashboard by navigating to the Job list page and clicking the "triple-dot" menu next to the job you want to delete:
![Job Menu](/images/job-triple-dot-menu.png)
This will bring up a dialog confirming that you want to delete the job and all of its history:
![Delete Job Dialog](/images/delete-job.png)
+4
View File
@@ -258,5 +258,9 @@ yarn dlx @trigger.dev/cli@latest dev
You can optionally pass the port if you're not running on 3000 by adding
`--port 3001` to the end
</Note>
<Note>
You can optionally pass the hostname if you're not running on localhost by adding
`--hostname <host>`. Example, in case your Next.js is running on 0.0.0.0: `--hostname 0.0.0.0`.
</Note>
<Tip>If your existing Next.js project utilizes middleware and you encounter any issues, such as potential conflicts with Trigger.dev, it's recommended to refer to the troubleshooting guide at [Middleware](/documentation/guides/platforms/nextjs#middleware) for assistance. This guide can help you address any concerns related to middleware conflicts and ensure the smooth functioning of your project with Trigger.dev.</Tip>
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+5 -2
View File
@@ -93,8 +93,11 @@ Tasks that are marked as "long-running" can last longer than your serverless tim
| `listFineTunes` | Lists the available fine-tunes. |
| `retrieveFineTune` | Retrieves a specific fine-tune by ID. |
| `cancelFineTune` | Cancels a specific fine-tune by ID. |
| `listFineTuneEvents` | Lists the events for a specific fine-tune by ID. |
| `deleteFineTune` | Deletes a specific fine-tune by ID. |
| `createFineTuningJob` | Creates a job that fine-tunes a specified model from a given dataset. |
| `retrieveFineTuningJob` | Get info about a fine-tuning job. |
| `cancelFineTuningJob` | Cancel a fine-tuning job |
| `listFineTuningJobEvents` | List events for a fine-tuning job |
| `listFineTuningJobs` | List fine tuning jobs |
## Examples
+34 -11
View File
@@ -63,7 +63,10 @@
"documentation/introduction",
{
"group": "Quick Starts",
"pages": ["documentation/quickstarts/nextjs", "documentation/quickstarts/supabase"]
"pages": [
"documentation/quickstarts/nextjs",
"documentation/quickstarts/supabase"
]
},
"documentation/guides/create-a-job",
"documentation/guides/video-walkthrough"
@@ -114,6 +117,7 @@
"documentation/guides/cli",
"documentation/guides/manual",
"documentation/guides/running-jobs",
"documentation/guides/jobs/managing",
{
"group": "Using the Dashboard",
"pages": [
@@ -167,7 +171,10 @@
},
{
"group": "Overview",
"pages": ["integrations/introduction", "integrations/create"]
"pages": [
"integrations/introduction",
"integrations/create"
]
},
{
"group": "Integrations",
@@ -180,23 +187,30 @@
"integrations/apis/github-tasks"
]
},
{
"group": "OpenAI",
"pages": ["integrations/apis/openai"]
"pages": [
"integrations/apis/openai"
]
},
"integrations/apis/plain",
{
"group": "Resend",
"pages": ["integrations/apis/resend"]
"pages": [
"integrations/apis/resend"
]
},
{
"group": "SendGrid",
"pages": ["integrations/apis/sendgrid"]
"pages": [
"integrations/apis/sendgrid"
]
},
{
"group": "Slack",
"pages": ["integrations/apis/slack"]
"pages": [
"integrations/apis/slack"
]
},
{
"group": "Supabase",
@@ -258,7 +272,10 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
}
]
},
@@ -269,7 +286,10 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
}
]
},
@@ -290,7 +310,10 @@
},
{
"group": "Overview",
"pages": ["examples/introduction", "examples/examples-repository"]
"pages": [
"examples/introduction",
"examples/examples-repository"
]
}
],
"footerSocials": {
@@ -303,4 +326,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
+3 -21
View File
@@ -49,27 +49,6 @@ client.defineJob({
});
```
```ts queue options
client.defineJob({
id: "github-integration-on-issue",
name: "GitHub Integration - On Issue",
version: "0.1.0",
trigger: github.triggers.repo({
event: events.onIssue,
owner: "triggerdotdev",
repo: "empty",
}),
queue: {
name: "my-queue",
maxConcurrent: 10, // only 10 runs can happen at the same time
},
run: async (payload, io, ctx) => {
await io.logger.info("This is a simple log info message");
return { payload, ctx };
},
});
```
</RequestExample>
# Constructor
@@ -114,6 +93,9 @@ client.defineJob({
Imports the specified integrations into the Job. The integrations will be available on the `io` object in the `run()` function with the same name as the key. For example:
<Snippet file="how-to-pass-integrations.mdx" />
</ParamField>
<ParamField body="enabled" type="boolean">
The `enabled` property is an optional property that specifies whether the Job is enabled or not. The Job will be enabled by default if you omit this property. When a job is disabled, no new runs will be triggered or resumed. In progress runs will continue to run until they are finished or delayed by using `io.wait`.
</ParamField>
<ParamField body="logLevel" type="log | error | warn | info | debug">
The `logLevel` property is an optional property that specifies the level of
logging for the Job. The level is inherited from the client if you omit this property.
+1
View File
@@ -10,6 +10,7 @@
"supabase": "nodemon --watch src/supabase.ts -r tsconfig-paths/register -r dotenv/config src/supabase.ts",
"supabase:types": "npx supabase gen types typescript --project-id $SUPABASE_PROJECT_ID --schema public --schema auth --schema storage > src/supabase-types.ts",
"events": "nodemon --watch src/events.ts -r tsconfig-paths/register -r dotenv/config src/events.ts",
"schedules": "nodemon --watch src/schedules.ts -r tsconfig-paths/register -r dotenv/config src/schedules.ts",
"stressTest": "nodemon --watch src/stressTest.ts -r tsconfig-paths/register -r dotenv/config src/stressTest.ts",
"delays": "nodemon --watch src/delays.ts -r tsconfig-paths/register -r dotenv/config src/delays.ts",
"dev:trigger": "trigger-cli dev --port 8080"
+27
View File
@@ -13,6 +13,7 @@ client.defineJob({
id: "event-example-1",
name: "Event Example 1",
version: "1.0.0",
enabled: true,
trigger: eventTrigger({
name: "event.example",
}),
@@ -29,4 +30,30 @@ client.defineJob({
},
});
client.defineJob({
id: "cancel-event-example",
name: "Cancel Event Example",
version: "1.0.0",
trigger: eventTrigger({
name: "cancel.event.example",
}),
run: async (payload, io, ctx) => {
await io.sendEvent(
"send-event",
{ name: "Cancellable Event", id: payload.id, payload: { payload, ctx } },
{
deliverAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours from now
}
);
await io.getEvent("get-event", payload.id);
await io.wait("wait-1", 60); // 1 minute
await io.cancelEvent("cancel-event", payload.id);
await io.getEvent("get-event-2", payload.id);
},
});
createExpressServer(client);
+2 -2
View File
@@ -28,9 +28,9 @@ client.defineJob({
run: async (payload, io, ctx) => {
const models = await io.openai.listModels("list-models");
if (models.data.length > 0) {
if (models.length > 0) {
await io.openai.retrieveModel("get-model", {
model: models.data[0].id,
model: models[0].id,
});
}
+33
View File
@@ -0,0 +1,33 @@
import { createExpressServer } from "@trigger.dev/express";
import { TriggerClient, intervalTrigger } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "job-catalog",
apiKey: process.env["TRIGGER_API_KEY"],
apiUrl: process.env["TRIGGER_API_URL"],
verbose: false,
ioLogLocalEnabled: true,
});
client.defineJob({
id: "schedule-example-1",
name: "Schedule Example 1",
version: "1.0.0",
enabled: true,
trigger: intervalTrigger({
seconds: 60 * 3, // 3 minutes
}),
run: async (payload, io, ctx) => {
await io.runTask("task-example-1", { name: "Task 1" }, async () => {
return {
message: "Hello World",
};
});
await io.wait("wait-1", 1);
await io.logger.info("Hello World", { ctx });
},
});
createExpressServer(client);
+5
View File
@@ -129,10 +129,13 @@ client.defineJob({
},
});
// Use the stripe CLI to test this job:
// stripe trigger price.created
client.defineJob({
id: "stripe-on-price",
name: "Stripe On Price",
version: "0.1.0",
enabled: true,
trigger: stripe.onPrice({ events: ["price.created", "price.updated"] }),
run: async (payload, io, ctx) => {
if (ctx.event.name === "price.created") {
@@ -143,6 +146,8 @@ client.defineJob({
},
});
// Use the stripe CLI to test this job:
// stripe trigger product.created
client.defineJob({
id: "stripe-on-product",
name: "Stripe On Product",
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/github
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "2.0.10",
"version": "2.0.11",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -29,8 +29,8 @@
"@octokit/request": "^6.2.5",
"@octokit/request-error": "^4.0.1",
"@octokit/webhooks": "^10.4.0",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.10",
"@trigger.dev/sdk": "workspace:^2.0.11",
"@trigger.dev/integration-kit": "workspace:^2.0.11",
"octokit": "^2.0.14",
"zod": "3.21.4"
},
+12
View File
@@ -1,5 +1,17 @@
# @trigger.dev/slack
## 2.0.11
### Patch Changes
- 403acac5: #344 Upgrading the openai package to v4
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "2.0.10",
"version": "2.0.11",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,12 +24,11 @@
"build:tsup": "tsup"
},
"dependencies": {
"openai": "^3.3.0",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.10",
"zod": "3.21.4"
"openai": "^4.2.0",
"@trigger.dev/sdk": "workspace:^2.0.11",
"@trigger.dev/integration-kit": "workspace:^2.0.11"
},
"engines": {
"node": ">=16.8.0"
}
}
}
+5 -7
View File
@@ -1,5 +1,5 @@
import type { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk";
import { Configuration, OpenAIApi } from "openai";
import OpenAIApi from "openai";
import * as tasks from "./tasks";
import { OpenAIIntegrationOptions } from "./types";
@@ -28,12 +28,10 @@ export class OpenAI implements TriggerIntegration<IntegrationClient<OpenAIApi, t
throw `Can't create OpenAI integration (${options.id}) as apiKey was undefined`;
}
this.native = new OpenAIApi(
new Configuration({
apiKey: options.apiKey,
organization: options.organization,
})
);
this.native = new OpenAIApi({
apiKey: options.apiKey,
organization: options.organization,
});
this.client = {
tasks,
+3 -24
View File
@@ -1,8 +1,7 @@
import { CreateCompletionResponseUsage, CreateEmbeddingResponseUsage } from "openai";
import { z } from "zod";
import OpenAI from "openai";
export function createTaskUsageProperties(
usage: CreateCompletionResponseUsage | CreateEmbeddingResponseUsage | undefined
usage: OpenAI.Completions.CompletionUsage | OpenAI.CreateEmbeddingResponse.Usage | undefined
) {
if (!usage) {
return;
@@ -28,26 +27,6 @@ export function createTaskUsageProperties(
];
}
const OpenAIErrorSchema = z.object({
response: z.object({
data: z.object({
error: z.object({
code: z.string().nullable().optional(),
message: z.string(),
type: z.string(),
}),
}),
}),
});
export function onTaskError(error: unknown) {
const openAIError = OpenAIErrorSchema.safeParse(error);
if (!openAIError.success) {
return;
}
const { message, code, type } = openAIError.data.response.data.error;
return new Error(`${type}: ${message}${code ? ` (${code})` : ""}`);
return;
}
+250 -166
View File
@@ -1,36 +1,20 @@
import type { AuthenticatedTask } from "@trigger.dev/sdk";
import {
CreateChatCompletionRequest,
CreateCompletionRequest,
CreateEditRequest,
CreateEmbeddingRequest,
CreateFineTuneRequest,
CreateImageRequest,
OpenAIApi,
} from "openai";
import OpenAI from "openai";
import { OpenAIIntegrationAuth } from "./types";
import { redactString } from "@trigger.dev/sdk";
import { Prettify, fileFromString, fileFromUrl, truncate } from "@trigger.dev/integration-kit";
import { createTaskUsageProperties, onTaskError } from "./taskUtils";
type OpenAIClientType = InstanceType<typeof OpenAIApi>;
type RetrieveModelRequest = {
model: string;
};
type RetrieveModelResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["retrieveModel"]>>["data"]
>;
type OpenAIClientType = InstanceType<typeof OpenAI>;
export const retrieveModel: AuthenticatedTask<
OpenAIClientType,
Prettify<RetrieveModelRequest>,
RetrieveModelResponseData
{ model: string },
OpenAI.Models.Model
> = {
onError: onTaskError,
run: async (params, client) => {
return client.retrieveModel(params.model).then((res) => res.data);
return await client.models.retrieve(params.model);
},
init: (params) => {
return {
@@ -47,16 +31,12 @@ export const retrieveModel: AuthenticatedTask<
},
};
type ListModelsResponseData = Awaited<ReturnType<OpenAIClientType["listModels"]>>["data"];
export const listModels: AuthenticatedTask<
OpenAIClientType,
void,
Prettify<ListModelsResponseData>
> = {
export const listModels: AuthenticatedTask<OpenAIClientType, void, OpenAI.Models.Model[]> = {
onError: onTaskError,
run: async (params, client) => {
return client.listModels().then((res) => res.data);
const response = await client.models.list();
return response.data;
},
init: (params) => {
return {
@@ -70,15 +50,15 @@ export const listModels: AuthenticatedTask<
export const createCompletion: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateCompletionRequest>,
Prettify<Awaited<ReturnType<OpenAIClientType["createCompletion"]>>["data"]>
Prettify<OpenAI.CompletionCreateParamsNonStreaming>,
OpenAI.Completion
> = {
run: async (params, client, task) => {
const response = await client.createCompletion(params);
const response = await client.completions.create(params);
task.outputProperties = createTaskUsageProperties(response.data.usage);
task.outputProperties = createTaskUsageProperties(response.usage);
return response.data;
return response;
},
init: (params) => {
return {
@@ -95,18 +75,14 @@ export const createCompletion: AuthenticatedTask<
},
};
type CreateCompletionResponseData = Awaited<
ReturnType<OpenAIClientType["createCompletion"]>
>["data"];
export const backgroundCreateCompletion: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateCompletionRequest>,
Prettify<CreateCompletionResponseData>,
Prettify<OpenAI.CompletionCreateParamsNonStreaming>,
Prettify<OpenAI.Completion>,
OpenAIIntegrationAuth
> = {
run: async (params, client, task, io, auth) => {
const response = await io.backgroundFetch<CreateCompletionResponseData>(
const response = await io.backgroundFetch<OpenAI.Completion>(
"background",
"https://api.openai.com/v1/completions",
{
@@ -139,22 +115,18 @@ export const backgroundCreateCompletion: AuthenticatedTask<
},
};
type CreateChatCompetionResponseData = Awaited<
ReturnType<OpenAIClientType["createChatCompletion"]>
>["data"];
export const createChatCompletion: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateChatCompletionRequest>,
Prettify<CreateChatCompetionResponseData>
Prettify<OpenAI.Chat.CompletionCreateParamsNonStreaming>,
Prettify<OpenAI.Chat.ChatCompletion>
> = {
onError: onTaskError,
run: async (params, client, task) => {
const response = await client.createChatCompletion(params);
const response = await client.chat.completions.create(params);
task.outputProperties = createTaskUsageProperties(response.data.usage);
task.outputProperties = createTaskUsageProperties(response.usage);
return response.data;
return response;
},
init: (params) => {
return {
@@ -173,12 +145,12 @@ export const createChatCompletion: AuthenticatedTask<
export const backgroundCreateChatCompletion: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateChatCompletionRequest>,
Prettify<CreateChatCompetionResponseData>,
Prettify<OpenAI.Chat.CompletionCreateParamsNonStreaming>,
Prettify<OpenAI.Chat.ChatCompletion>,
OpenAIIntegrationAuth
> = {
run: async (params, client, task, io, auth) => {
const response = await io.backgroundFetch<CreateChatCompetionResponseData>(
const response = await io.backgroundFetch<OpenAI.Chat.ChatCompletion>(
"background",
"https://api.openai.com/v1/chat/completions",
{
@@ -229,20 +201,21 @@ export const backgroundCreateChatCompletion: AuthenticatedTask<
},
};
type CreateEditResponseData = Prettify<Awaited<ReturnType<OpenAIClientType["createEdit"]>>["data"]>;
/**
* @deprecated The Edits API is deprecated; please use Chat Completions instead.
*/
export const createEdit: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateEditRequest>,
CreateEditResponseData
Prettify<OpenAI.EditCreateParams>,
OpenAI.Edit
> = {
onError: onTaskError,
run: async (params, client, task) => {
const response = await client.createEdit(params);
const response = await client.edits.create(params);
task.outputProperties = createTaskUsageProperties(response.data.usage);
task.outputProperties = createTaskUsageProperties(response.usage);
return response.data;
return response;
},
init: (params) => {
let properties = [
@@ -273,20 +246,16 @@ export const createEdit: AuthenticatedTask<
},
};
type CreateImageResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createImage"]>>["data"]
>;
export const createImage: AuthenticatedTask<
export const generateImage: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateImageRequest>,
CreateImageResponseData
Prettify<OpenAI.Images.ImageGenerateParams>,
OpenAI.Images.ImagesResponse
> = {
onError: onTaskError,
run: async (params, client, task) => {
const response = await client.createImage(params);
const response = await client.images.generate(params);
return response.data;
return response;
},
init: (params) => {
let properties = [
@@ -326,6 +295,8 @@ export const createImage: AuthenticatedTask<
},
};
export const createImage = generateImage;
export type CreateImageEditRequest = {
image: string | File;
prompt: string;
@@ -336,31 +307,27 @@ export type CreateImageEditRequest = {
user?: string;
};
type CreateImageEditResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createImageEdit"]>>["data"]
>;
export const createImageEdit: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateImageEditRequest>,
CreateImageEditResponseData
OpenAI.Images.ImagesResponse
> = {
onError: onTaskError,
run: async (params, client, task) => {
const file = typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const mask = typeof params.mask === "string" ? await fileFromUrl(params.mask) : params.mask;
const response = await client.createImageEdit(
file,
params.prompt,
mask,
params.n,
params.size,
params.response_format,
params.user
);
const response = await client.images.edit({
image: file,
prompt: params.prompt,
mask: mask,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
});
return response.data;
return response;
},
init: (params) => {
let properties = [];
@@ -408,10 +375,6 @@ export const createImageEdit: AuthenticatedTask<
},
};
type CreateImageVariationResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createImageVariation"]>>["data"]
>;
export type CreateImageVariationRequest = {
image: string | File;
n?: number;
@@ -423,15 +386,19 @@ export type CreateImageVariationRequest = {
export const createImageVariation: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateImageVariationRequest>,
CreateImageVariationResponseData
OpenAI.Images.ImagesResponse
> = {
onError: onTaskError,
run: async (params, client, task) => {
const file = typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const response = await client
.createImageVariation(file, params.n, params.size, params.response_format, params.user)
.then((res) => res.data);
const response = await client.images.createVariation({
image: file,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
});
return response;
},
@@ -476,22 +443,18 @@ export const createImageVariation: AuthenticatedTask<
},
};
type CreateEmbeddingResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createEmbedding"]>>["data"]
>;
export const createEmbedding: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateEmbeddingRequest>,
CreateEmbeddingResponseData
Prettify<OpenAI.EmbeddingCreateParams>,
OpenAI.Embeddings.CreateEmbeddingResponse
> = {
onError: onTaskError,
run: async (params, client, task) => {
const response = await client.createEmbedding(params);
const response = await client.embeddings.create(params);
task.outputProperties = createTaskUsageProperties(response.data.usage);
task.outputProperties = createTaskUsageProperties(response.usage);
return response.data;
return response;
},
init: (params) => {
return {
@@ -503,20 +466,11 @@ export const createEmbedding: AuthenticatedTask<
label: "Model",
text: params.model,
},
{
label: "Input",
text:
typeof params.input === "string"
? truncate(params.input, 40)
: truncate(params.input.at(0) ?? "none", 40),
},
],
};
},
};
type CreateFileResponseData = Awaited<ReturnType<OpenAIClientType["createFile"]>>["data"];
type CreateFileRequest = {
file: string | File;
fileName?: string;
@@ -526,7 +480,7 @@ type CreateFileRequest = {
export const createFile: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateFileRequest>,
Prettify<CreateFileResponseData>
Prettify<OpenAI.Files.FileObject>
> = {
onError: onTaskError,
run: async (params, client) => {
@@ -538,7 +492,7 @@ export const createFile: AuthenticatedTask<
file = params.file;
}
return client.createFile(file, params.purpose).then((res) => res.data);
return client.files.create({ file, purpose: params.purpose });
},
init: (params) => {
return {
@@ -559,12 +513,12 @@ export const createFile: AuthenticatedTask<
},
};
type ListFilesResponseData = Prettify<Awaited<ReturnType<OpenAIClientType["listFiles"]>>["data"]>;
export const listFiles: AuthenticatedTask<OpenAIClientType, void, ListFilesResponseData> = {
export const listFiles: AuthenticatedTask<OpenAIClientType, void, OpenAI.Files.FileObject[]> = {
onError: onTaskError,
run: async (params, client) => {
return client.listFiles().then((res) => res.data);
const response = await client.files.list();
return response.data;
},
init: (params) => {
return {
@@ -587,7 +541,7 @@ type CreateFineTuneFileRequest = {
export const createFineTuneFile: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateFineTuneFileRequest>,
Prettify<CreateFileResponseData>
Prettify<OpenAI.Files.FileObject>
> = {
onError: onTaskError,
run: async (params, client) => {
@@ -596,7 +550,7 @@ export const createFineTuneFile: AuthenticatedTask<
params.fileName
);
return client.createFile(file, "fine-tune").then((res) => res.data);
return client.files.create({ file, purpose: "fine-tune" });
},
init: (params) => {
return {
@@ -613,18 +567,14 @@ export const createFineTuneFile: AuthenticatedTask<
},
};
type CreateFineTuneResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["createFineTune"]>>["data"]
>;
export const createFineTune: AuthenticatedTask<
OpenAIClientType,
Prettify<CreateFineTuneRequest>,
CreateFineTuneResponseData
Prettify<OpenAI.FineTuneCreateParams>,
OpenAI.FineTunes.FineTune
> = {
onError: onTaskError,
run: async (params, client) => {
return client.createFineTune(params).then((res) => res.data);
return client.fineTunes.create(params);
},
init: (params) => {
let properties = [
@@ -657,41 +607,36 @@ export const createFineTune: AuthenticatedTask<
},
};
type ListFineTunesResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["listFineTunes"]>>["data"]
>;
export const listFineTunes: AuthenticatedTask<OpenAIClientType, void, OpenAI.FineTunes.FineTune[]> =
{
onError: onTaskError,
run: async (params, client) => {
const response = await client.fineTunes.list();
export const listFineTunes: AuthenticatedTask<OpenAIClientType, void, ListFineTunesResponseData> = {
onError: onTaskError,
run: async (params, client) => {
return client.listFineTunes().then((res) => res.data);
},
init: (params) => {
return {
name: "List fine tunes",
params,
icon: "openai",
properties: [],
};
},
};
return response.data;
},
init: (params) => {
return {
name: "List fine tunes",
params,
icon: "openai",
properties: [],
};
},
};
type SpecificFineTuneRequest = {
fineTuneId: string;
};
type RetrieveFineTuneResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["retrieveFineTune"]>>["data"]
>;
export const retrieveFineTune: AuthenticatedTask<
OpenAIClientType,
Prettify<SpecificFineTuneRequest>,
RetrieveFineTuneResponseData
OpenAI.FineTunes.FineTune
> = {
onError: onTaskError,
run: async (params, client) => {
return client.retrieveFineTune(params.fineTuneId).then((res) => res.data);
return client.fineTunes.retrieve(params.fineTuneId);
},
init: (params) => {
return {
@@ -708,18 +653,14 @@ export const retrieveFineTune: AuthenticatedTask<
},
};
type CancelFineTuneResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["cancelFineTune"]>>["data"]
>;
export const cancelFineTune: AuthenticatedTask<
OpenAIClientType,
Prettify<SpecificFineTuneRequest>,
CancelFineTuneResponseData
OpenAI.FineTunes.FineTune
> = {
onError: onTaskError,
run: async (params, client) => {
return client.cancelFineTune(params.fineTuneId).then((res) => res.data);
return client.fineTunes.cancel(params.fineTuneId);
},
init: (params) => {
return {
@@ -736,18 +677,14 @@ export const cancelFineTune: AuthenticatedTask<
},
};
type ListFineTuneEventsResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["listFineTuneEvents"]>>["data"]
>;
export const listFineTuneEvents: AuthenticatedTask<
OpenAIClientType,
Prettify<SpecificFineTuneRequest>,
ListFineTuneEventsResponseData
OpenAI.FineTuneEventsListResponse
> = {
onError: onTaskError,
run: async (params, client) => {
return client.listFineTuneEvents(params.fineTuneId, false).then((res) => res.data);
return client.fineTunes.listEvents(params.fineTuneId, { stream: false });
},
init: (params) => {
return {
@@ -768,18 +705,14 @@ type DeleteFineTunedModelRequest = {
fineTunedModelId: string;
};
type DeleteFineTuneResponseData = Prettify<
Awaited<ReturnType<OpenAIClientType["deleteModel"]>>["data"]
>;
export const deleteFineTune: AuthenticatedTask<
OpenAIClientType,
Prettify<DeleteFineTunedModelRequest>,
DeleteFineTuneResponseData
OpenAI.Models.ModelDeleted
> = {
onError: onTaskError,
run: async (params, client) => {
return client.deleteModel(params.fineTunedModelId).then((res) => res.data);
return client.models.del(params.fineTunedModelId);
},
init: (params) => {
return {
@@ -795,3 +728,154 @@ export const deleteFineTune: AuthenticatedTask<
};
},
};
/**
* Creates a job that fine-tunes a specified model from a given dataset.
*
* Response includes details of the enqueued job including job status and the name
* of the fine-tuned models once complete.
*
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
*/
export const createFineTuningJob: AuthenticatedTask<
OpenAIClientType,
Prettify<OpenAI.FineTuning.JobCreateParams>,
OpenAI.FineTuning.FineTuningJob
> = {
onError: onTaskError,
run: async (params, client) => {
return client.fineTuning.jobs.create(params);
},
init: (params) => {
let properties = [
{
label: "File ID",
text: params.training_file,
},
];
if (params.model) {
properties.push({
label: "Model",
text: params.model,
});
}
if (params.validation_file) {
properties.push({
label: "Validation file",
text: params.validation_file,
});
}
return {
name: "Create Fine Tuning Job",
params,
icon: "openai",
properties,
};
},
};
/**
* Get info about a fine-tuning job.
*
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
*/
export const retrieveFineTuningJob: AuthenticatedTask<
OpenAIClientType,
{ id: string },
OpenAI.FineTuning.FineTuningJob
> = {
onError: onTaskError,
run: async (params, client) => {
return client.fineTuning.jobs.retrieve(params.id);
},
init: (params) => {
return {
name: "Retrieve Fine Tuning Job",
params,
icon: "openai",
properties: [
{
label: "Job ID",
text: params.id,
},
],
};
},
};
export const cancelFineTuningJob: AuthenticatedTask<
OpenAIClientType,
{ id: string },
OpenAI.FineTuning.FineTuningJob
> = {
onError: onTaskError,
run: async (params, client) => {
return client.fineTuning.jobs.cancel(params.id);
},
init: (params) => {
return {
name: "Cancel Fine Tuning Job",
params,
icon: "openai",
properties: [
{
label: "Job ID",
text: params.id,
},
],
};
},
};
export const listFineTuningJobEvents: AuthenticatedTask<
OpenAIClientType,
{ id: string },
OpenAI.FineTuning.FineTuningJobEvent[]
> = {
onError: onTaskError,
run: async (params, client) => {
const response = await client.fineTuning.jobs.listEvents(params.id);
return response.data;
},
init: (params) => {
return {
name: "List Fine Tuning Job Events",
params,
icon: "openai",
properties: [
{
label: "Job ID",
text: params.id,
},
],
};
},
};
/**
* List your organization's fine-tuning jobs
*/
export const listFineTuningJobs: AuthenticatedTask<
OpenAIClientType,
OpenAI.FineTuning.JobListParams,
OpenAI.FineTuning.FineTuningJob[]
> = {
onError: onTaskError,
run: async (params, client) => {
const response = await client.fineTuning.jobs.list(params);
return response.data;
},
init: (params) => {
return {
name: "List Fine Tuning Jobs",
params,
icon: "openai",
properties: [],
};
},
};
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/plain
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "2.0.10",
"version": "2.0.11",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.0.10",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.11",
"@trigger.dev/sdk": "workspace:^2.0.11",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/resend
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "2.0.10",
"version": "2.0.11",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.0.10",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.11",
"@trigger.dev/sdk": "workspace:^2.0.11",
"resend": "^0.9.1"
},
"engines": {
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/sendgrid
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "2.0.10",
"version": "2.0.11",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.10"
"@trigger.dev/sdk": "workspace:^2.0.11",
"@trigger.dev/integration-kit": "workspace:^2.0.11"
},
"engines": {
"node": ">=16.8.0"
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/slack
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
## 2.0.10
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "2.0.10",
"version": "2.0.11",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/sdk": "workspace:^2.0.11",
"zod": "3.21.4"
},
"engines": {
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/stripe
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "2.0.10",
"version": "2.0.11",
"description": "Trigger.dev integration for stripe",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.0.10",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.11",
"@trigger.dev/sdk": "workspace:^2.0.11",
"stripe": "^12.14.0",
"zod": "3.21.4"
},
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/supabase
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "2.0.10",
"version": "2.0.11",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^2.0.10",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.11",
"@trigger.dev/sdk": "workspace:^2.0.11",
"supabase-management-js": "^0.1.4",
"zod": "3.21.4"
},
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/typeform
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
- @trigger.dev/integration-kit@2.0.11
## 2.0.10
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "2.0.10",
"version": "2.0.11",
"description": "The official Typeform integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@typeform/api-client": "^1.8.0",
"@trigger.dev/sdk": "workspace:^2.0.10",
"@trigger.dev/integration-kit": "workspace:^2.0.10",
"@trigger.dev/sdk": "workspace:^2.0.11",
"@trigger.dev/integration-kit": "workspace:^2.0.11",
"zod": "3.21.4"
},
"engines": {
+11
View File
@@ -0,0 +1,11 @@
# @trigger.dev/astro
## 2.0.11
### Patch Changes
- Updated dependencies [ac98219b]
- Updated dependencies [302bd02f]
- Updated dependencies [b5db9f5e]
- Updated dependencies [3ce53970]
- @trigger.dev/sdk@2.0.11
+23
View File
@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2023 Trigger.dev
Permission is hereby granted, free of
charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+34
View File
@@ -0,0 +1,34 @@
# Trigger.dev native integration add-on for Astro
This Astro integration project provides a `createAstroRoute` function that can be used to create an API endpoint route in your Astro project for integration with the hosted Trigger.dev platform.
## Usage
1. Install the package:
```bash
npm install --save @trigger.dev/astro
```
Note: yes, right now this package isn't published to the npm registry, so you'll need to install it from my GitHub repository if you want to experiment with it. Hopefully this soon graduates into an official npm package from the Trigger.dev team 🙏🏼
2. Import the package in a `src/pages/api/trigger.js` file and define the route endpoint as follows:
```js
import { createAstroRoute } from "triggerdev-astro-integration";
import { client } from "../../../trigger.js";
export const post = createAstroRoute(client);
```
## More information
See the [Trigger.dev Astro example project repository](https://github.com/lirantal/trigger.dev-astro-example) for a working example of this integration.
## License
MIT
## Author
(c) Liran Tal <liran@lirantal.com>
+4
View File
@@ -0,0 +1,4 @@
import { defineConfig } from "astro/config";
// https://astro.build/config
export default defineConfig({});
+48
View File
@@ -0,0 +1,48 @@
export function createAstroRoute(client) {
return async function astroRoute(ctx) {
if (ctx.request.method === "HEAD") {
return new Response(null, { status: 200 });
}
try {
// Prepare the request to be a fetch-compatible Request object:
const requestHeaders = ctx.request.headers;
const requestMethod = ctx.request.method;
const responseHeaders = Object.create(null);
for (const [headerName, headerValue] of requestHeaders.entries()) {
responseHeaders[headerName] = headerValue;
}
// Create a new Request object to be passed to the TriggerClient
// where we pass the clone the incoming request metadata such as
// headers, method, body.
const request = new Request("https://express.js/api/trigger", {
headers: responseHeaders,
method: requestMethod,
body: ctx.request.body ? ctx.request.body : ctx.request,
duplex: "half",
});
// This handshake handler knows how to authenticate requests,
// call the run() function of the job, and so on
const response = await client.handleRequest(request);
if (!response) {
return new Response(JSON.stringify({ error: "Not found" }), {
status: 404,
});
}
// Optionally can do something with the job's finished
// execution's response body
return new Response(JSON.stringify(response.body), {
status: response.status,
});
} catch (err) {
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
});
}
};
}
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@trigger.dev/astro",
"description": "An Astro-native integration for Trigger.dev background jobs platform",
"version": "2.0.11",
"type": "module",
"main": "main.js",
"scripts": {},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^2.0.11",
"astro": "^2.10.7"
},
"engines": {
"node": ">=18.0.0"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/triggerdev/trigger.dev.git"
},
"bugs": {
"url": "https://github.com/triggerdev/trigger.dev/issues"
},
"homepage": "https://github.com/triggerdev/trigger.dev#readme"
}
+12
View File
@@ -1,5 +1,17 @@
# create-trigger
## 2.0.11
### Patch Changes
- 3ce53970: Added the send-event command
- 3897e6e6: Make it more clear which API key the init command expects
- b5db9f5e: Adding MIT license
- dd107176: Added hostname option to the cli dev command
- 8cf85443: Bugfix: @trigger.dev/cli init now correctly identifies the App Dir when using JS
- 4e78da31: fix: Add an update sub-command the @trigger.dev/cli that updates all @trigger.dev/\* packages
- 135cb492: fixed the cli init log message to show the correct path to the app route created
## 2.0.10
### Patch Changes

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