From 9c6838e5c88f4022a821e0f2733592670ec0337c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 6 Nov 2023 15:32:40 +0000 Subject: [PATCH] HTTP endpoints, HTTP triggers and new sidebar (#720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Setup basic storybook page * storybook preview pages don’t have padding * Added a storybook story for the side menu * WIP on a new side menu component * New variant for the side menu button style * Side menu items can now recieve a badge ‘count’ * Storybook side menu update * Added a new font size * you can now set the spacing between an icon and the text * Moving the error and count badge to the right side of the button * Added a menu sub item button style * added sub items to the menu and all the rest of the items * Proper background class used * Added a side menu header and main org header * Removed the no-mobile blocker overlay so you can use the site on a phone * Better section name * Created a cusom popover trigger that can take anything as children. Also the popover titles can now take a variant * Added a popover for the user avatar menu item * Removed the tool tip * Arrow Popover can no also go full width * neater conditiona; * Removed unnessary div. And error icon can display an error message * menu item buttons can take a variant * Added placeholder items for the org dropdown * add a max height to the org menu popover and handle scrollbars * Added a border if the menu scrolls when you have lots of project items * warning badges display tooltips if you give them some error text * Removed the sticky header so the scrollbar looks better * Fixed a layout issue from removing the sticky header * Better spacing between lists * Popover can take any react component icon and an icon classname * Side menu headers now have a dropdown by clicking the ellipsis icon * Side menu items can take an icon colour * Updated styling for the dropdown Popover menus * Installed Tabler icons * Updated some icons to use Tabler * Started defining the HttpTrigger class * RequestFilterSchema is part of core * Use the RequestFilterSchema from core * EventSpecificationExample now derived from schema * Need to define the class like EventTrigger * Catalog reference that uses the httpTrigger * The logic for the httpTrigger is in the separate file, just call it from the client using defineHttpTrigger * Basic HttpTrigger * Import fixes * Transforming httpTrigger to the format we want * Initial httpTrigger types working * Added HTTP generic type * Added some notes * Rejigged some things to support verifying payloads * Use new security function for verifying * Added truncating text support if side menu names get long * Renamed some stuff * Removed test copy * HttpEndpoin that isn’t a trigger * Renamed hostname to source * Created HttpEndpoint as a top-level concept, it produces triggers using onRequest * Started working on registration of the httpEndpoints * Removed the new TriggerMetadata for now, http will use static * Added PayloadType to EventRecord. Default is JSON. * The payload from an HTTPEndpoint is now a Request * Initial TriggerHttpEndpoint migrations * Initial server side work * Migration to add shortcode to RuntimeEnvironment * Added TriggerHttpEndpointEnvironment table * Made the httptrigger catalog example runnable * Progress creating/updating httpEndpoints * HTTPEndpointEnvironment updating now works * Added a note about EventRecord HttpEndpoint relationship * When creating environments, create a shortcode * Fix for secret not being retrieved correctly * Request filtering * Add DOM types to Core * Early work dealing with a webhook arriving * Added some todo notes to discuss tomorrow * Added relations to the endpoint so we can call the user’s server * Basic handling of immediate responses * The immediate responses are now going back to the webhook provider * Handling is now generating events and responding where relevant. Not yet matching filters and doing verification * Added source to TriggerHttpEndpointEnvironment * The event filter is now optional * The source is passed through from the httpEndpointEnv * Added optional payloadType when ingesting an event * Added tabler * Updated @remix-run/testing usage for remix 2 * Fixed org menu header width and added org and project types * Added custom icons for Runs and Endpoints * Updated the menu icons * storybook side menu shows static project and org data * Updated side menu * Plumbed in paths * Updated eventFilter to deal with undefined/null payloads * Started work on filtering, but it’s not working * Strict null * async filtering it working * Added environment shortcodes to the seedCloud script * Don’t pass the secret back, added a verifyPayload function that gets called before runs start * Fix for skipping triggering runs * The verify callback is now passed through to the trigger so it can be called * If verifying fails, have a reason which we can show to the user * Don’t verify if it’s a test run * Fixed the log in the example code * Added loader and action to storybook preview * Fix for svg attribute name issues in EndpointIcon * Started work on the side menu * This logic has moved to a separate file, in a different branch * Don’t get the environments when getting orgs * Lots of fixes for the new SideMenu * Refactored some org/project things to fit the new structure better * Some progress restructuring the app for the new sidebar * More changes to data loading * Deleted some old pages. The redirecting is working if you’re on the root but logged in * Basic styling for the account page. We need a back button * Fixes for the integrations pages * Breadcrumb implemented * Add a border to the bottom of the breadcrumb * Feedback is working, not in Storybook * Removed expandSidebar from the Handle * Org level breadcrumbs * Getting the project in the org route now works properly * orgslug route before using the presenter * More UI improvements * Removed commented out line * Use presenters for org and root * Removed forceActive * Split the project selector and user menu out * The side menu items have correct active states * Fixed the scrolling * Put the org name in * Improved the admin features * Created the HTTP Endpoints list page * Work on the HTTP endpoint page * WIP on the HTTP Endpoint page * Fix for response filter not showing * Change page URLs to “http-endpoints” (from httpendpoints) * Slugify the HTTP endpoint id * Added todos for EventRecord associations * Removed the Job/Trigger tab for now * First draft of the help panels * Rejigged the HTTP endpoint page so it splits out the deployed and not deployed environments * Removed irrelevant columns * Added a link to the deployment guide * Removed the background image from the frameworks pages Also… - reordered the frameworks so supported ones come first in the list - added logos to 3 framework pages that were missing them - removed lots of un-used imports * Fixed an issue where the breadcrumb could be hidden on small screens so broken the pages The breadcrumb is always visible now * Renamed ‘Onboarding’ to ‘Framework setup’ * Added a new http-endpoint icon to the side menu * Side menu now has a nice looking scrollbar * Scrollbar on the usage and billing page updated to the nice style * Icon and colour updates to the side bar menu items * Small copy improvement * Fixed bug with the height of the org menu dropdown * Page breadcrumbs now don’t include duplicate path links * renamed uiBorder to ui-border * Remove the padding from the small menu item if it’s used as a button * Made the paragraph text a bit smaller * EventRecord has relationships with TriggerHttpEndpoint/TriggerHttpEndpointEnvironment * Improved the presenter used by the side menu. Also added the HTTP Endpoint badge count to the sidebar * The dropdown org menu has the same badge as the side menu * Improved the Projects page * Fixed button style miss-match in the jobs table dropdown * Made the popover dialogs support Headers and improved the regenerate API key popover * Made the active status of a job more subtle * Allow the Job to have a link on its Trigger * Simplified loader because we ditched the Jobs menu. Also showing no runs help panel now * Fixed type errors * Pages that are org level can scroll if the content gets too long * Fixed a redirect bug so you now redirect to the org page * Updated the shortcode migration, to add additional letters to the end * Removed the nav bar from the welcome onboarding page * Only show the countBadge if the number is > 0 * Jobs icon now has a default colour the same as the nav menu * Capital I for integration * Clone the request before calling verify * Separate the checks out so they can be easily logged * Make the failure reason optional * Made the verification error look better, if it’s provided * Working WhatsApp example * Can set the immediateResponseFilter to null in an update * Deal with a null immediateResponseFilter * Cal.com webhook working * Header height reduced slightly * Fixed button padding * Removed un-used imports * http endpoint table displays a message if you have no entries in the table * Removed some duplicate text and swapped primary buttons for secondary style * All ‘HTTP Endpoints’ now ‘HTTP endpoints’ * You have to use a RequestFilter when using HTTP Trigger * Synchronous eventDispatcher filtering again. Better content-type check * Stripe example * Side menu “Usage & Billing” is just “Usage” if self-hosting * Fix for filtering not working * Removed filter log * Fix for jobs being optional and null * Removed old nav bar from the invite page * Deleted old nav bar component * import cleanup * NoTask message only displays if the run has status “SUCCESS” * Added a global page loading indicator * Added documentation buttons to the most important pages * Added GitHub * Use “verifyRequestSignature” helpr * Removed todo * Fixed the layout of the Rerun Job popover * Jobs table has margin bottom so it sits off the bottom of the page * Small improvement to the search bar layout on the integration page * HTTP endpoint and Trigger concept docs * Reference docs and JSDocs for HTTP endpoint * SDK reference docs for HttpEndpoint * Added an HttpTrigger SDK reference, which mainly just point you elsewhere * Improved the copy in the help panels * Removed unused code * Improved HTTP endpoint comment about setting the secret * Delete unused asyncFilter * Fixed the delete modal not having a title * Always try get the arrayBuffer for an HTTP request, catch and use undefined as the default * Add extra method to invokeTrigger * Use a transaction for the secret store * verifyRequestSignature docs page and JSDocs * Added verifyRequestSignature docs page to nav * Warn if they use the same HttpEndpoint id twice * Added another job using the same HttpEndpoint * Renamed the api route to http-endpoints from httpendpoints * Debug bar for VSCode is now in the commandCenter --------- Co-authored-by: James Ritchie --- .vscode/settings.json | 3 +- apps/webapp/.storybook/preview.tsx | 3 + apps/webapp/app/assets/icons/EndpointIcon.tsx | 36 ++ .../app/assets/icons/IntegrationIcon.tsx | 5 + apps/webapp/app/assets/icons/RunsIcon.tsx | 18 + .../app/components/ImpersonationBanner.tsx | 22 +- .../environments/RegenerateApiKeyModal.tsx | 100 ++-- .../frameworks/FrameworkComingSoon.tsx | 73 ++- .../frameworks/FrameworkSelector.tsx | 87 ++-- .../helpContent/HelpContentText.tsx | 141 +++++- .../components/jobs/DeleteJobModalContent.tsx | 2 +- apps/webapp/app/components/jobs/JobsTable.tsx | 21 +- .../app/components/layout/AppLayout.tsx | 22 +- .../app/components/navigation/Breadcrumb.tsx | 32 +- .../app/components/navigation/JobsMenu.tsx | 65 --- .../app/components/navigation/NavBar.tsx | 47 -- .../navigation/PageNavigationIndicator.tsx | 9 + .../components/navigation/ProjectSideMenu.tsx | 206 --------- .../components/navigation/ProjectsMenu.tsx | 79 ---- .../app/components/navigation/SideMenu.tsx | 426 ++++++++++++++++++ .../app/components/primitives/Buttons.tsx | 64 ++- .../app/components/primitives/Dialog.tsx | 6 +- .../app/components/primitives/FormTitle.tsx | 2 +- .../webapp/app/components/primitives/Help.tsx | 7 +- .../app/components/primitives/NamedIcon.tsx | 19 +- .../app/components/primitives/PageHeader.tsx | 34 +- .../app/components/primitives/Paragraph.tsx | 5 + .../app/components/primitives/Popover.tsx | 78 +++- .../app/components/primitives/Sheet.tsx | 8 +- .../app/components/primitives/Spinner.tsx | 6 +- .../app/components/primitives/Table.tsx | 8 +- .../webapp/app/components/primitives/Tabs.tsx | 2 +- .../webapp/app/components/run/RunOverview.tsx | 55 ++- apps/webapp/app/components/runs/RunsTable.tsx | 2 +- apps/webapp/app/hooks/useFilterJobs.ts | 2 +- .../webapp/app/hooks/useIntegrationClient.tsx | 4 +- ...ojectChildPage.ts => useIsOrgChildPage.ts} | 4 +- apps/webapp/app/hooks/useJob.tsx | 5 +- apps/webapp/app/hooks/useJobs.tsx | 28 -- apps/webapp/app/hooks/useOrganizations.ts | 8 +- apps/webapp/app/models/organization.server.ts | 57 +-- .../EnvironmentsPresenter.server.ts | 18 +- .../HttpEndpointPresenter.server.ts | 154 +++++++ .../HttpEndpointsPresenter.server.ts | 64 +++ ...rationClientConnectionsPresenter.server.ts | 3 - .../IntegrationClientPresenter.server.ts | 6 +- ...IntegrationClientScopesPresenter.server.ts | 2 - .../IntegrationsPresenter.server.ts | 6 +- .../app/presenters/JobListPresenter.server.ts | 27 +- .../app/presenters/JobPresenter.server.ts | 234 ++++++++++ .../NewOrganizationPresenter.server.ts | 20 + .../OrganizationsPresenter.server.ts | 117 +++++ .../app/presenters/ProjectPresenter.server.ts | 10 +- .../SelectBestProjectPresenter.server.ts | 49 ++ apps/webapp/app/routes/_app._index/route.tsx | 19 + .../OrganizationGrid.tsx | 73 --- .../routes/_app._orgaccount._index/route.tsx | 62 --- .../app/routes/_app._orgaccount/route.tsx | 49 -- .../OrgAdminHeader.tsx | 55 --- .../route.tsx | 32 +- .../route.tsx | 29 +- .../route.tsx | 236 +++++----- .../route.tsx | 6 +- .../route.tsx | 6 +- .../route.tsx | 6 +- .../route.tsx | 17 +- .../route.tsx | 23 +- .../route.tsx | 25 +- .../FirstEndpointSheet.tsx | 9 +- .../route.tsx | 38 +- .../route.tsx | 236 ++++++++++ .../route.tsx | 136 ++++++ .../route.tsx | 11 + .../route.tsx | 25 +- .../route.tsx | 4 +- .../route.tsx | 23 +- .../route.tsx | 2 +- .../route.tsx | 73 ++- .../route.tsx | 5 - .../route.tsx | 142 +++--- .../route.tsx | 160 ++++--- .../route.tsx | 2 +- .../route.tsx | 185 ++++---- .../route.tsx | 337 +++++++------- .../route.tsx | 2 +- .../route.tsx | 2 +- .../route.tsx | 144 +++--- .../route.tsx | 158 ++++--- .../route.tsx | 4 +- .../route.tsx | 11 +- .../route.tsx | 3 +- .../route.tsx | 4 +- .../route.tsx | 4 +- .../route.tsx | 2 +- .../route.tsx | 32 +- .../route.tsx | 16 +- .../_app.orgs.$organizationSlug/route.tsx | 106 +++-- .../webapp/app/routes/_app.orgs.new/route.tsx | 43 +- apps/webapp/app/routes/_app/route.tsx | 58 +-- .../route.tsx | 110 +++-- apps/webapp/app/routes/admin._index.tsx | 8 +- .../routes/api.v1.endpointindex.$indexId.ts | 7 +- ...$httpEndpointId.env.$envType.$shortcode.ts | 56 +++ .../app/routes/confirm-basic-details.tsx | 10 +- apps/webapp/app/routes/invites.tsx | 15 +- .../services/currentOrganization.server.ts | 44 -- .../app/services/currentProject.server.ts | 33 ++ .../webapp/app/services/endpointApi.server.ts | 31 ++ .../endpoints/performEndpointIndexService.ts | 21 +- .../app/services/environmentSort.server.ts | 20 + .../services/events/deliverEvent.server.ts | 44 +- .../services/events/ingestSendEvent.server.ts | 15 +- .../httpendpoint/HandleHttpEndpointService.ts | 241 ++++++++++ .../app/services/impersonation.server.ts | 2 +- .../app/services/jobs/registerJob.server.ts | 4 +- .../sources/handleHttpSource.server.ts | 15 +- apps/webapp/app/services/telemetry.server.ts | 5 +- .../triggers/registerHttpEndpoint.server.ts | 114 +++++ .../app/utils/createHttpSourceRequest.ts | 17 + apps/webapp/app/utils/handle.ts | 1 - apps/webapp/app/utils/pathBuilder.ts | 45 +- apps/webapp/package.json | 2 + apps/webapp/prisma/seedCloud.ts | 2 + apps/webapp/remix.config.js | 1 + apps/webapp/tailwind.config.js | 12 +- docs/_snippets/request-filter.mdx | 36 ++ .../documentation/concepts/http-endpoints.mdx | 86 ++++ docs/documentation/concepts/triggers/http.mdx | 107 +++++ .../concepts/triggers/introduction.mdx | 14 +- .../concepts/triggers/webhooks.mdx | 5 +- docs/images/http-endpoint.png | Bin 0 -> 283287 bytes docs/images/http-endpoints-list.png | Bin 0 -> 412925 bytes docs/mint.json | 44 +- docs/sdk/http-endpoint.mdx | 70 +++ docs/sdk/http-trigger.mdx | 54 +++ .../instancemethods/define-http-endpoint.mdx | 128 ++++++ docs/sdk/triggerclient/overview.mdx | 4 + docs/sdk/verify-request-signature.mdx | 67 +++ packages/core/src/eventFilterMatches.ts | 8 + packages/core/src/index.ts | 1 + packages/core/src/requestFilterMatches.ts | 47 ++ packages/core/src/schemas/api.ts | 28 ++ packages/core/src/schemas/eventFilter.ts | 22 +- packages/core/src/schemas/index.ts | 2 + packages/core/src/schemas/request.ts | 8 + packages/core/src/schemas/requestFilter.ts | 60 +++ packages/core/src/schemas/triggers.ts | 11 + packages/core/test/eventFilterMatches.test.ts | 14 + packages/core/tsconfig.json | 3 +- .../migration.sql | 5 + .../migration.sql | 29 ++ .../migration.sql | 19 + .../migration.sql | 46 ++ .../migration.sql | 34 ++ .../migration.sql | 2 + .../migration.sql | 15 + .../migration.sql | 2 + .../migration.sql | 8 + .../migration.sql | 8 + .../migration.sql | 9 + .../migration.sql | 9 + .../migration.sql | 3 + packages/database/prisma/schema.prisma | 131 ++++-- packages/trigger-sdk/src/httpEndpoint.ts | 215 +++++++++ packages/trigger-sdk/src/index.ts | 1 + packages/trigger-sdk/src/security.ts | 48 ++ packages/trigger-sdk/src/triggerClient.ts | 176 +++++++- packages/trigger-sdk/src/triggers/dynamic.ts | 4 + .../trigger-sdk/src/triggers/eventTrigger.ts | 4 + .../src/triggers/externalSource.ts | 4 + .../trigger-sdk/src/triggers/invokeTrigger.ts | 12 +- .../trigger-sdk/src/triggers/notifications.ts | 12 + .../trigger-sdk/src/triggers/scheduled.ts | 12 + packages/trigger-sdk/src/types.ts | 26 +- pnpm-lock.yaml | 53 ++- references/job-catalog/package.json | 2 + references/job-catalog/src/httpTrigger.ts | 171 +++++++ 177 files changed, 5623 insertions(+), 2257 deletions(-) create mode 100644 apps/webapp/app/assets/icons/EndpointIcon.tsx create mode 100644 apps/webapp/app/assets/icons/IntegrationIcon.tsx create mode 100644 apps/webapp/app/assets/icons/RunsIcon.tsx delete mode 100644 apps/webapp/app/components/navigation/JobsMenu.tsx delete mode 100644 apps/webapp/app/components/navigation/NavBar.tsx create mode 100644 apps/webapp/app/components/navigation/PageNavigationIndicator.tsx delete mode 100644 apps/webapp/app/components/navigation/ProjectSideMenu.tsx delete mode 100644 apps/webapp/app/components/navigation/ProjectsMenu.tsx create mode 100644 apps/webapp/app/components/navigation/SideMenu.tsx rename apps/webapp/app/hooks/{useIsProjectChildPage.ts => useIsOrgChildPage.ts} (69%) delete mode 100644 apps/webapp/app/hooks/useJobs.tsx create mode 100644 apps/webapp/app/presenters/HttpEndpointPresenter.server.ts create mode 100644 apps/webapp/app/presenters/HttpEndpointsPresenter.server.ts create mode 100644 apps/webapp/app/presenters/JobPresenter.server.ts create mode 100644 apps/webapp/app/presenters/NewOrganizationPresenter.server.ts create mode 100644 apps/webapp/app/presenters/OrganizationsPresenter.server.ts create mode 100644 apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts create mode 100644 apps/webapp/app/routes/_app._index/route.tsx delete mode 100644 apps/webapp/app/routes/_app._orgaccount._index/OrganizationGrid.tsx delete mode 100644 apps/webapp/app/routes/_app._orgaccount._index/route.tsx delete mode 100644 apps/webapp/app/routes/_app._orgaccount/route.tsx delete mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug._index/OrgAdminHeader.tsx rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.integrations => _app.orgs.$organizationSlug.integrations}/route.tsx (74%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index => _app.orgs.$organizationSlug.integrations_.$clientParam._index}/route.tsx (94%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.connections => _app.orgs.$organizationSlug.integrations_.$clientParam.connections}/route.tsx (93%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.scopes => _app.orgs.$organizationSlug.integrations_.$clientParam.scopes}/route.tsx (87%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam => _app.orgs.$organizationSlug.integrations_.$clientParam}/route.tsx (86%) create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints.$httpEndpointParam/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints._index/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints/route.tsx rename apps/webapp/app/routes/{_app._orgaccount.account => account}/route.tsx (59%) create mode 100644 apps/webapp/app/routes/api.v1.http-endpoints.$httpEndpointId.env.$envType.$shortcode.ts delete mode 100644 apps/webapp/app/services/currentOrganization.server.ts create mode 100644 apps/webapp/app/services/currentProject.server.ts create mode 100644 apps/webapp/app/services/environmentSort.server.ts create mode 100644 apps/webapp/app/services/httpendpoint/HandleHttpEndpointService.ts create mode 100644 apps/webapp/app/services/triggers/registerHttpEndpoint.server.ts create mode 100644 apps/webapp/app/utils/createHttpSourceRequest.ts create mode 100644 docs/_snippets/request-filter.mdx create mode 100644 docs/documentation/concepts/http-endpoints.mdx create mode 100644 docs/documentation/concepts/triggers/http.mdx create mode 100644 docs/images/http-endpoint.png create mode 100644 docs/images/http-endpoints-list.png create mode 100644 docs/sdk/http-endpoint.mdx create mode 100644 docs/sdk/http-trigger.mdx create mode 100644 docs/sdk/triggerclient/instancemethods/define-http-endpoint.mdx create mode 100644 docs/sdk/verify-request-signature.mdx create mode 100644 packages/core/src/requestFilterMatches.ts create mode 100644 packages/core/src/schemas/request.ts create mode 100644 packages/core/src/schemas/requestFilter.ts create mode 100644 packages/database/prisma/migrations/20231023144342_added_event_record_payload_type_default_is_json/migration.sql create mode 100644 packages/database/prisma/migrations/20231023173456_added_trigger_http_endpoint_table/migration.sql create mode 100644 packages/database/prisma/migrations/20231024122540_http_endpoint_scoped_to_project_not_environment/migration.sql create mode 100644 packages/database/prisma/migrations/20231024122736_runtime_environment_shortcodes/migration.sql create mode 100644 packages/database/prisma/migrations/20231025091939_trigger_http_endpoint_environment_created/migration.sql create mode 100644 packages/database/prisma/migrations/20231026100653_added_skip_triggering_runs_to_trigger_http_endpoint_environment/migration.sql create mode 100644 packages/database/prisma/migrations/20231026103218_add_endpoint_relation_to_http_endpoint_environment/migration.sql create mode 100644 packages/database/prisma/migrations/20231026103614_trigger_http_endpoint_environment_removed_the_environment_constraint/migration.sql create mode 100644 packages/database/prisma/migrations/20231026104408_trigger_http_endpoint_environment_added_the_environment_constraint_back_in/migration.sql create mode 100644 packages/database/prisma/migrations/20231026165235_trigger_http_endpoint_environment_added_source/migration.sql create mode 100644 packages/database/prisma/migrations/20231101202056_trigger_http_endpoint_environment_added_created_at_updated_at/migration.sql create mode 100644 packages/database/prisma/migrations/20231102143530_added_relationships_between_event_record_and_trigger_http_endpoint_trigger_http_endpoint_environment/migration.sql create mode 100644 packages/database/prisma/migrations/20231102171207_job_version_added_trigger_link_and_trigger_help/migration.sql create mode 100644 packages/trigger-sdk/src/httpEndpoint.ts create mode 100644 packages/trigger-sdk/src/security.ts create mode 100644 references/job-catalog/src/httpTrigger.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f5239032..f7666be0e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "deno.enablePaths": ["references/deno-reference"] + "deno.enablePaths": ["references/deno-reference"], + "debug.toolBarLocation": "commandCenter" } diff --git a/apps/webapp/.storybook/preview.tsx b/apps/webapp/.storybook/preview.tsx index a995aea73..14932b462 100644 --- a/apps/webapp/.storybook/preview.tsx +++ b/apps/webapp/.storybook/preview.tsx @@ -23,12 +23,15 @@ const preview: Preview = { }, ], }, + layout: "fullscreen", }, decorators: [ (Story) => { const RemixStub = createRemixStub([ { path: "/*", + action: () => ({ redirect: "/" }), + loader: () => ({ redirect: "/" }), Component: Story, }, ]); diff --git a/apps/webapp/app/assets/icons/EndpointIcon.tsx b/apps/webapp/app/assets/icons/EndpointIcon.tsx new file mode 100644 index 000000000..d491e25a8 --- /dev/null +++ b/apps/webapp/app/assets/icons/EndpointIcon.tsx @@ -0,0 +1,36 @@ +export function EndpointIcon({ className }: { className?: string }) { + return ( + + + + + + + ); +} diff --git a/apps/webapp/app/assets/icons/IntegrationIcon.tsx b/apps/webapp/app/assets/icons/IntegrationIcon.tsx new file mode 100644 index 000000000..fd1839228 --- /dev/null +++ b/apps/webapp/app/assets/icons/IntegrationIcon.tsx @@ -0,0 +1,5 @@ +import { LogoIcon } from "~/components/LogoIcon"; + +export function IntegrationIcon() { + return ; +} diff --git a/apps/webapp/app/assets/icons/RunsIcon.tsx b/apps/webapp/app/assets/icons/RunsIcon.tsx new file mode 100644 index 000000000..8688058a6 --- /dev/null +++ b/apps/webapp/app/assets/icons/RunsIcon.tsx @@ -0,0 +1,18 @@ +export function RunsIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/apps/webapp/app/components/ImpersonationBanner.tsx b/apps/webapp/app/components/ImpersonationBanner.tsx index d665d344b..d4486fc96 100644 --- a/apps/webapp/app/components/ImpersonationBanner.tsx +++ b/apps/webapp/app/components/ImpersonationBanner.tsx @@ -1,24 +1,18 @@ import { Form } from "@remix-run/react"; import { Paragraph } from "./primitives/Paragraph"; import { Button } from "./primitives/Buttons"; +import { UserMinusIcon } from "@heroicons/react/20/solid"; -export function ImpersonationBanner({ impersonationId }: { impersonationId: string }) { +export function ImpersonationBanner() { return ( -
- - - You are impersonating {impersonationId} - -
+
+ diff --git a/apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx b/apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx index f94e01cfc..86caba799 100644 --- a/apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx +++ b/apps/webapp/app/components/environments/RegenerateApiKeyModal.tsx @@ -1,13 +1,14 @@ +import { ArrowPathIcon } from "@heroicons/react/20/solid"; +import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; +import { useFetcher } from "@remix-run/react"; +import { useState } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog"; +import { generateTwoRandomWords } from "~/utils/randomWords"; import { Button } from "../primitives/Buttons"; -import { Dialog, DialogContent, DialogTrigger } from "~/components/primitives/Dialog"; -import { Header1, Header2 } from "../primitives/Headers"; +import { Header1 } from "../primitives/Headers"; import { Input } from "../primitives/Input"; -import { NamedIcon } from "../primitives/NamedIcon"; import { Paragraph } from "../primitives/Paragraph"; import { Spinner } from "../primitives/Spinner"; -import { useState } from "react"; -import { useFetcher } from "@remix-run/react"; -import { generateTwoRandomWords } from "~/utils/randomWords"; type ModalProps = { id: string; @@ -25,11 +26,16 @@ export function RegenerateApiKeyModal({ id, title }: ModalProps) { return ( - + {`Regenerate ${title} Environment Key`} { +const RegenerateApiKeyModalContent = ({ id, randomWord, title, closeModal }: ModalContentProps) => { const [confirmationText, setConfirmationText] = useState(""); const fetcher = useFetcher(); const isSubmitting = fetcher.state === "submitting"; @@ -52,55 +58,47 @@ const RegenerateApiKeyModalContent = ({ id, title, randomWord, closeModal }: Mod } return ( -
-
- {title} Environment +
+
+ + + Regenerating the keys for this environment will temporarily break any live Jobs in the + {title} Environment until the new API keys are set + in the relevant environment variables. +
- - Are you sure you want to regenerate the keys
- for this environment? -
- - <> - This will temporarily break any live jobs in the - {title} Environment until the new API keys - are set in the relevant environment variables. - - - - To confirm, please enter the text:{" "} - {randomWord} - - setConfirmationText(e.target.value)} - /> - +
+ Enter this text below to confirm: + + {randomWord} + +
+
+ setConfirmationText(e.target.value)} + className="rounded-r-none" + variant="large" + /> + +
); diff --git a/apps/webapp/app/components/frameworks/FrameworkComingSoon.tsx b/apps/webapp/app/components/frameworks/FrameworkComingSoon.tsx index c17848a11..da30882e4 100644 --- a/apps/webapp/app/components/frameworks/FrameworkComingSoon.tsx +++ b/apps/webapp/app/components/frameworks/FrameworkComingSoon.tsx @@ -3,11 +3,10 @@ import { GitHubDarkIcon } from "@trigger.dev/companyicons"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { projectSetupPath } from "~/utils/pathBuilder"; -import { PageGradient } from "../PageGradient"; import { LinkButton } from "../primitives/Buttons"; import { Header1 } from "../primitives/Headers"; -import { Paragraph } from "../primitives/Paragraph"; import { NamedIcon } from "../primitives/NamedIcon"; +import { Paragraph } from "../primitives/Paragraph"; export type FrameworkComingSoonProps = { frameworkName: string; githubIssueUrl: string; @@ -25,43 +24,41 @@ export function FrameworkComingSoon({ const project = useProject(); return ( - -
-
{children}
-
- {frameworkName} is coming soon! - - Choose a different framework - -
- - We're working hard to bring support for {frameworkName} in Trigger.dev. Follow along with - the GitHub issue or contribute and help us bring it to Trigger.dev faster. - - +
{children}
+
- + + We're working hard to bring support for {frameworkName} in Trigger.dev. Follow along with + the GitHub issue or contribute and help us bring it to Trigger.dev faster. + + + triggerdotdev/trigger.dev +

+ #{githubIssueNumber}Framework: + support for {frameworkName} +

+
+ + View on GitHub + +
+
+
); } diff --git a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx index 3ce50b1e5..02715df96 100644 --- a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx +++ b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx @@ -24,7 +24,6 @@ import { projectSetupSvelteKitPath, } from "~/utils/pathBuilder"; import { Feedback } from "../Feedback"; -import { PageGradient } from "../PageGradient"; import { Button } from "../primitives/Buttons"; import { Header1 } from "../primitives/Headers"; @@ -33,51 +32,49 @@ export function FrameworkSelector() { const project = useProject(); return ( - -
-
- Choose a framework to get started… - - Request a framework - - } - defaultValue="feature" - /> -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+ Choose a framework to get started… + + Request a framework + + } + defaultValue="feature" + />
- + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
); } diff --git a/apps/webapp/app/components/helpContent/HelpContentText.tsx b/apps/webapp/app/components/helpContent/HelpContentText.tsx index ce057dae4..393e1a41f 100644 --- a/apps/webapp/app/components/helpContent/HelpContentText.tsx +++ b/apps/webapp/app/components/helpContent/HelpContentText.tsx @@ -4,8 +4,7 @@ import { StepNumber } from "~/components/primitives/StepNumber"; import { useJob } from "~/hooks/useJob"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { IntegrationIcon } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route"; -import { jobTestPath } from "~/utils/pathBuilder"; +import { docsPath, jobTestPath } from "~/utils/pathBuilder"; import { CodeBlock } from "../code/CodeBlock"; import { InlineCode } from "../code/InlineCode"; import { EnvironmentLabel } from "../environments/EnvironmentLabel"; @@ -21,6 +20,7 @@ import selectEnvironment from "./select-environment.png"; import selectExample from "./select-example.png"; import { StepContentContainer } from "../StepContentContainer"; import { TriggerDevCommand } from "../SetupCommands"; +import { IntegrationIcon } from "~/assets/icons/IntegrationIcon"; export function HowToRunYourJob() { const organization = useOrganization(); @@ -294,3 +294,140 @@ export function HowToUseApiKeysAndEndpoints() { ); } + +export function WhatAreHttpEndpoints() { + return ( + <> + + HTTP endpoints allow you to trigger your Jobs from any webhooks. They require a bit more + work than using Integrations{" "} + but allow you to connect to any API. + + Getting started + + You need to define the HTTP endpoint in your code. To do this you use{" "} + client.defineHttpEndpoint(). This will create an HTTP endpoint. + + + Then you can create a Trigger from this by calling .onRequest() on + the created HTTP endpoint. + + + Read the HTTP endpoints guide to learn more. + + + An example: cal.com + + { + //this helper function makes verifying most webhooks easy + return await verifyRequestSignature({ + request, + headerName: "X-Cal-Signature-256", + secret: process.env.CALDOTCOM_SECRET!, + algorithm: "sha256", + }); + }, +}); + +client.defineJob({ + id: "http-caldotcom", + name: "HTTP Cal.com", + version: "1.0.0", + enabled: true, + //create a Trigger from the HTTP endpoint above. The filter is optional. + trigger: caldotcom.onRequest({ filter: { body: { triggerEvent: ["BOOKING_CANCELLED"] } } }), + run: async (request, io, ctx) => { + //note that when using HTTP endpoints, the first parameter is the request + //you need to get the body, usually it will be json so you do: + const body = await request.json(); + await io.logger.info("Body", body); + }, +});`} + /> + + ); +} + +export function HowToConnectHttpEndpoint() { + return ( + <> + Setting up your webhook + + To start receiving data you need to enter the Endpoint URL and secret into the API service + you want to receive webhooks from. + + + Go to the relevant API dashboard} /> + + + For example, if you want to receive webhooks from Cal.com then you should login to your + Cal.com account and go to their Settings/Developer/Webhooks page. + + + + Copy the Webhook URL and Secret} /> + + + A unique Webhook URL is created for each environment (Dev, Staging, and Prod). Jobs will + only be triggered from the relevant environment. + + + Copy the relevant Endpoint URL and secret from the table opposite and paste it into the + correct place in the API dashboard you located in the previous step. + + + + Add the Secret to your Environment variables} /> + + + You should also add the Secret to the Environment variables in your code and where you're + deploying. Usually in Node this means adding it to the .env file. + + + Use the secret in the verify() function of HTTP Endpoint. This + ensures that someone can't just send a request to your Endpoint and trigger a Job. + Different APIs do this verification in different ways – a common way is to have a header + that has a hash of the payload and secret. Refer to the API's documentation for more + information. + + + + Triggering runs + + + + In your code, you should use the .onRequest() function in a Job + Trigger. You can filter so only data that matches your criteria triggers the Job. + + + + + + If you're using the Staging or Prod environment, you need to make sure your code is + deployed. Deploy like you normally would –{" "} + + read our deployment guide + + . + + + + + + Now you need to actually perform an action on that third-party service that triggers the + webhook you've subscribed to. For example, add a new meeting using Cal.com. + + + + + Read the HTTP endpoints guide to learn more. + + + ); +} diff --git a/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx b/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx index f9fdf2775..eccd9ff31 100644 --- a/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx +++ b/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx @@ -40,7 +40,7 @@ export function DeleteJobDialogContent({ return (
-
+
{title} ID: {slug}
diff --git a/apps/webapp/app/components/jobs/JobsTable.tsx b/apps/webapp/app/components/jobs/JobsTable.tsx index 8f2e4fb84..5f2978f3c 100644 --- a/apps/webapp/app/components/jobs/JobsTable.tsx +++ b/apps/webapp/app/components/jobs/JobsTable.tsx @@ -1,7 +1,7 @@ -import { ProjectJob } from "~/hooks/useJobs"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { JobRunStatus } from "~/models/job.server"; +import { ProjectJob } from "~/presenters/JobListPresenter.server"; import { jobPath, jobTestPath } from "~/utils/pathBuilder"; import { Button } from "../primitives/Buttons"; import { DateTime } from "../primitives/DateTime"; @@ -30,7 +30,7 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul const project = useProject(); return ( - +
Job @@ -160,19 +160,18 @@ export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResul - - - - + Delete Job + diff --git a/apps/webapp/app/components/layout/AppLayout.tsx b/apps/webapp/app/components/layout/AppLayout.tsx index d1099ead1..413c481da 100644 --- a/apps/webapp/app/components/layout/AppLayout.tsx +++ b/apps/webapp/app/components/layout/AppLayout.tsx @@ -11,7 +11,7 @@ export function AppContainer({ }) { return ( -
{children}
+
{children}
); } @@ -47,7 +47,11 @@ export function PageContainer({ children: React.ReactNode; className?: string; }) { - return
{children}
; + return ( +
+ {children} +
+ ); } export function PageBody({ @@ -58,7 +62,15 @@ export function PageBody({ scrollable?: boolean; }) { return ( -
{children}
+
+ {children} +
); } @@ -68,8 +80,8 @@ export function PageBodyPadding({ children }: { children: React.ReactNode }) { export function MainCenteredContainer({ children }: { children: React.ReactNode }) { return ( -
-
{children}
+
+
{children}
); } diff --git a/apps/webapp/app/components/navigation/Breadcrumb.tsx b/apps/webapp/app/components/navigation/Breadcrumb.tsx index b2d5fcd2d..6cad225f0 100644 --- a/apps/webapp/app/components/navigation/Breadcrumb.tsx +++ b/apps/webapp/app/components/navigation/Breadcrumb.tsx @@ -2,6 +2,7 @@ import { UIMatch, useMatches } from "@remix-run/react"; import { Fragment, ReactNode } from "react"; import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon"; import { Handle } from "~/utils/handle"; +import { LinkButton } from "../primitives/Buttons"; export type BreadcrumbItem = (match: UIMatch, allMatches: UIMatch[]) => ReactNode; @@ -9,19 +10,26 @@ export function Breadcrumb() { const matches = useMatches() as UIMatch[]; return ( -
- {matches.map((match) => { - if (!match.handle || !match.handle.breadcrumb) return null; +
+ {matches + .filter((b) => b.handle && b.handle.breadcrumb) + .map((match, index) => { + const breadcrumb = match.handle.breadcrumb as BreadcrumbItem; - const breadcrumb = match.handle.breadcrumb as BreadcrumbItem; - - return ( - - - {breadcrumb(match, matches)} - - ); - })} + return ( + + {index !== 0 && } {breadcrumb(match, matches)} + + ); + })}
); } + +export function BreadcrumbLink({ title, to }: { title: string; to: string }) { + return ( + + {title} + + ); +} diff --git a/apps/webapp/app/components/navigation/JobsMenu.tsx b/apps/webapp/app/components/navigation/JobsMenu.tsx deleted file mode 100644 index 506910e88..000000000 --- a/apps/webapp/app/components/navigation/JobsMenu.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Link, UIMatch } from "@remix-run/react"; -import { useState } from "react"; -import { useJob } from "~/hooks/useJob"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { useJobs } from "~/hooks/useJobs"; -import { cn } from "~/utils/cn"; -import { jobPath } from "~/utils/pathBuilder"; -import { LabelValueStack } from "../primitives/LabelValueStack"; -import { NamedIcon } from "../primitives/NamedIcon"; -import { - Popover, - PopoverArrowTrigger, - PopoverContent, - PopoverSectionHeader, -} from "../primitives/Popover"; - -export function JobsMenu({ matches }: { matches: UIMatch[] }) { - const [isOpen, setIsOpen] = useState(false); - const organization = useOrganization(matches); - const project = useProject(matches); - const projectJobs = useJobs(matches); - const currentJob = useJob(matches); - - return ( - <> - setIsOpen(open)}> - - {currentJob?.title ?? "Select a job"} - - - -
- {projectJobs.map((job) => { - const isSelected = job.id === currentJob?.id; - return ( - - - - {isSelected && } - - ); - })} -
-
-
- - ); -} diff --git a/apps/webapp/app/components/navigation/NavBar.tsx b/apps/webapp/app/components/navigation/NavBar.tsx deleted file mode 100644 index 9f09cd72b..000000000 --- a/apps/webapp/app/components/navigation/NavBar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Popover, Transition } from "@headlessui/react"; -import { BookOpenIcon, ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid"; -import { Link } from "@remix-run/react"; -import { Fragment } from "react"; -import { cn } from "~/utils/cn"; -import { Feedback } from "../Feedback"; -import { LogoIcon } from "../LogoIcon"; -import { Button, LinkButton } from "../primitives/Buttons"; -import { Breadcrumb } from "./Breadcrumb"; -import { docsRoot } from "~/utils/pathBuilder"; - -export function NavBar() { - return ( -
-
- - - - -
-
- - Documentation - - - Help & feedback - - } - /> -
-
- ); -} - -export function BreadcrumbLink({ title, to }: { title: string; to: string }) { - return ( - - {title} - - ); -} diff --git a/apps/webapp/app/components/navigation/PageNavigationIndicator.tsx b/apps/webapp/app/components/navigation/PageNavigationIndicator.tsx new file mode 100644 index 000000000..3fb255ac5 --- /dev/null +++ b/apps/webapp/app/components/navigation/PageNavigationIndicator.tsx @@ -0,0 +1,9 @@ +import { useNavigation } from "@remix-run/react"; +import { Spinner } from "../primitives/Spinner"; + +export function PageNavigationIndicator() { + const navigation = useNavigation(); + if (navigation.state === "loading") { + return ; + } +} diff --git a/apps/webapp/app/components/navigation/ProjectSideMenu.tsx b/apps/webapp/app/components/navigation/ProjectSideMenu.tsx deleted file mode 100644 index 91a6cdf6d..000000000 --- a/apps/webapp/app/components/navigation/ProjectSideMenu.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { useMatches } from "@remix-run/react"; -import { motion } from "framer-motion"; -import { useOptionalJob } from "~/hooks/useJob"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { cn } from "~/utils/cn"; -import { Handle } from "~/utils/handle"; -import { - accountPath, - organizationBillingPath, - organizationTeamPath, - projectEnvironmentsPath, - projectIntegrationsPath, - projectPath, - projectSetupPath, - projectTriggersPath, -} from "~/utils/pathBuilder"; -import { UserProfilePhoto } from "../UserProfilePhoto"; -import { NavLinkButton } from "../primitives/Buttons"; -import { NamedIcon, type IconNames } from "../primitives/NamedIcon"; -import { SimpleTooltip } from "../primitives/Tooltip"; - -export function SideMenuContainer({ children }: { children: React.ReactNode }) { - return
{children}
; -} - -const expandedWith = "14rem"; -const collapsedWith = "2.81rem"; - -const menuVariants = { - expanded: { - minWidth: expandedWith, - width: expandedWith, - }, - collapsed: { - minWidth: collapsedWith, - width: collapsedWith, - }, -}; - -export function ProjectSideMenu() { - const organization = useOrganization(); - const project = useProject(); - const matches = useMatches(); - - //the deepest route `handle` determines if the menu is expanded - const deepestMatch = matches.at(-1); - const handle = deepestMatch?.handle as Handle; - const isCollapsed = handle?.expandSidebar ? !handle.expandSidebar : true; - - const job = useOptionalJob(); - const jobsActive = - job !== undefined || - deepestMatch?.id === "routes/_app.orgs.$organizationSlug.projects.$projectParam._index"; - - return ( - -
- - - - -
-
- - - - - -
-
- ); -} - -const itemVariants = { - expanded: { - opacity: 1, - }, - collapsed: { - opacity: 0, - }, -}; - -function SideMenuItem({ - icon, - name, - to, - isCollapsed, - forceActive, - hasWarning = false, - target, -}: { - icon: IconNames | React.ComponentType; - name: string; - to: string; - isCollapsed: boolean; - hasWarning?: boolean; - forceActive?: boolean; - target?: string; -}) { - return ( - { - if (forceActive !== undefined) { - isActive = forceActive; - } - return cn( - "relative", - isActive || isPending - ? "bg-slate-800 text-bright group-hover:bg-slate-800" - : "text-dimmed group-hover:bg-slate-850 group-hover:text-bright" - ); - }} - > - - {name} - - {hasWarning && } - - } - content={name} - side="right" - hidden={!isCollapsed} - /> - ); -} diff --git a/apps/webapp/app/components/navigation/ProjectsMenu.tsx b/apps/webapp/app/components/navigation/ProjectsMenu.tsx deleted file mode 100644 index eae72eb49..000000000 --- a/apps/webapp/app/components/navigation/ProjectsMenu.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { UIMatch } from "@remix-run/react"; -import { Fragment, useState } from "react"; -import simplur from "simplur"; -import { Badge } from "~/components/primitives/Badge"; -import { useIsNewOrganizationPage, useOrganizations } from "~/hooks/useOrganizations"; -import { useOptionalProject } from "~/hooks/useProject"; -import { newOrganizationPath, newProjectPath, projectPath } from "~/utils/pathBuilder"; -import { - Popover, - PopoverArrowTrigger, - PopoverContent, - PopoverMenuItem, - PopoverSectionHeader, -} from "../primitives/Popover"; - -export function ProjectsMenu({ matches }: { matches: UIMatch[] }) { - const [isOpen, setIsOpen] = useState(false); - const organizations = useOrganizations(matches); - const isNewOrgPage = useIsNewOrganizationPage(matches); - const currentProject = useOptionalProject(matches); - - if (isNewOrgPage) { - return null; - } - - return ( - <> - setIsOpen(open)}> - - {currentProject?.name ?? "Select a project"} - - - {organizations.map((organization) => ( - - -
- {organization.projects.map((project) => { - const isSelected = project.id === currentProject?.id; - return ( - - {project.name} - {simplur`${project._count.jobs} job[|s]`} -
- } - isSelected={isSelected} - icon="folder" - /> - ); - })} - -
- - ))} -
- -
- - - - ); -} diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx new file mode 100644 index 000000000..435cbb078 --- /dev/null +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -0,0 +1,426 @@ +import { + AcademicCapIcon, + ArrowRightOnRectangleIcon, + ChartBarIcon, + EllipsisHorizontalIcon, +} from "@heroicons/react/20/solid"; +import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid"; +import { useNavigation } from "@remix-run/react"; +import { IconExclamationCircle } from "@tabler/icons-react"; +import { AnchorHTMLAttributes, Fragment, useEffect, useRef, useState } from "react"; +import { MatchedOrganization } from "~/hooks/useOrganizations"; +import { usePathName } from "~/hooks/usePathName"; +import { MatchedProject } from "~/hooks/useProject"; +import { User } from "~/models/user.server"; +import { cn } from "~/utils/cn"; +import { + accountPath, + inviteTeamMemberPath, + logoutPath, + newOrganizationPath, + newProjectPath, + organizationBillingPath, + organizationIntegrationsPath, + organizationPath, + organizationTeamPath, + projectEnvironmentsPath, + projectHttpEndpointsPath, + projectPath, + projectSetupPath, + projectTriggersPath, +} from "~/utils/pathBuilder"; +import { Feedback } from "../Feedback"; +import { ImpersonationBanner } from "../ImpersonationBanner"; +import { LogoIcon } from "../LogoIcon"; +import { UserAvatar, UserProfilePhoto } from "../UserProfilePhoto"; +import { Button, LinkButton } from "../primitives/Buttons"; +import { Icon } from "../primitives/Icon"; +import { type IconNames } from "../primitives/NamedIcon"; +import { Paragraph } from "../primitives/Paragraph"; +import { + Popover, + PopoverArrowTrigger, + PopoverContent, + PopoverCustomTrigger, + PopoverMenuItem, + PopoverSectionHeader, +} from "../primitives/Popover"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; +import { useFeatures } from "~/hooks/useFeatures"; + +type SideMenuUser = Pick & { isImpersonating: boolean }; +type SideMenuProject = Pick< + MatchedProject, + "id" | "name" | "slug" | "hasInactiveExternalTriggers" | "jobCount" | "httpEndpointCount" +>; + +type SideMenuProps = { + user: SideMenuUser; + project: SideMenuProject; + organization: MatchedOrganization; + organizations: MatchedOrganization[]; +}; + +export function SideMenu({ user, project, organization, organizations }: SideMenuProps) { + const borderRef = useRef(null); + const [showHeaderDivider, setShowHeaderDivider] = useState(false); + const { isManagedCloud } = useFeatures(); + + useEffect(() => { + const handleScroll = () => { + if (borderRef.current) { + const shouldShowHeaderDivider = borderRef.current.scrollTop > 1; + if (showHeaderDivider !== shouldShowHeaderDivider) { + setShowHeaderDivider(shouldShowHeaderDivider); + } + } + }; + + borderRef.current?.addEventListener("scroll", handleScroll); + return () => borderRef.current?.removeEventListener("scroll", handleScroll); + }, [showHeaderDivider]); + + return ( +
+
+
+ + +
+
+
+ + + + + + + +
+
+ + + + + + + + +
+
+
+ + + + + Help & Feedback + + } + /> +
+
+
+ ); +} + +function ProjectSelector({ + project, + organization, + organizations, +}: { + project: SideMenuProject; + organization: MatchedOrganization; + organizations: MatchedOrganization[]; +}) { + const [isOrgMenuOpen, setOrgMenuOpen] = useState(false); + const navigation = useNavigation(); + + useEffect(() => { + setOrgMenuOpen(false); + }, [navigation.location?.pathname]); + + return ( + setOrgMenuOpen(open)} open={isOrgMenuOpen}> + + + {organization.title ?? "Select an organization"} + + + {organizations.map((organization) => ( + + +
+ {organization.projects.map((p) => { + const isSelected = p.id === project.id; + return ( + + {p.name} + +
+ } + isSelected={isSelected} + icon="folder" + /> + ); + })} + +
+ + ))} +
+ +
+ + + ); +} + +function UserMenu({ user }: { user: SideMenuUser }) { + const [isProfileMenuOpen, setProfileMenuOpen] = useState(false); + const navigation = useNavigation(); + + useEffect(() => { + setProfileMenuOpen(false); + }, [navigation.location?.pathname]); + + return ( + setProfileMenuOpen(open)}> + + + + + + +
+ {user.isImpersonating && } + {user.admin && ( + + )} + + +
+
+
+
+ ); +} + +function SideMenuHeader({ title, children }: { title: string; children: React.ReactNode }) { + const [isProjectMenuOpen, setProfileMenuOpen] = useState(false); + return ( +
+ + {title} + + setProfileMenuOpen(open)}> + + + + +
{children}
+
+
+
+ ); +} + +function SideMenuItem({ + icon, + iconColor, + name, + to, + hasWarning, + count, + target, + subItem = false, +}: { + icon?: IconNames | React.ComponentType; + iconColor?: string; + name: string; + to: string; + hasWarning?: string | boolean; + count?: number; + target?: AnchorHTMLAttributes["target"]; + subItem?: boolean; +}) { + const pathName = usePathName(); + const isActive = pathName === to; + + return ( + +
+ {name} +
+ {count !== undefined && count > 0 && } + {typeof hasWarning === "string" ? ( + + + + + + + {hasWarning} + + + + ) : ( + hasWarning && + )} +
+
+
+ ); +} + +function MenuCount({ count }: { count: number | string }) { + return
{count}
; +} diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index e469c2561..014b9096a 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -7,99 +7,129 @@ import { ShortcutKey } from "./ShortcutKey"; const variant = { "primary/small": { - textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80", + textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1", button: "h-6 px-[5px] text-xs bg-indigo-600 group-hover:bg-indigo-500/90 group-disabled:opacity-50 group-disabled:pointer-events-none", icon: "h-3.5", + iconSpacing: "gap-x-0.5", shortcutVariant: "small" as const, shortcut: "ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60 justify-self-center", }, "secondary/small": { - textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80", + textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1", button: "h-6 px-[5px] text-xs bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50 group-disabled:pointer-events-none", icon: "h-3.5", + iconSpacing: "gap-x-0.5", shortcutVariant: "small" as const, shortcut: "ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60", }, "tertiary/small": { - textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80", + textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1", button: "h-6 px-[5px] text-xs bg-transparent group-hover:bg-slate-850 disabled:opacity-50 group-disabled:pointer-events-none", icon: "h-3.5", + iconSpacing: "gap-x-0.5", shortcutVariant: "small" as const, shortcut: "ml-1 -mr-0.5 border-dimmed/40 text-dimmed group-hover:text-bright/80 group-hover:border-dimmed/60", }, "danger/small": { - textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80", + textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1", button: "h-6 px-[5px] text-xs bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50 group-disabled:pointer-events-none", icon: "h-3.5", + iconSpacing: "gap-x-0.5", shortcutVariant: "small" as const, shortcut: "ml-1 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60", }, "primary/medium": { - textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80", + textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1", button: "h-8 px-2 text-sm bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50", icon: "h-4", + iconSpacing: "gap-x-0.5", shortcutVariant: "medium" as const, shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60", }, "secondary/medium": { - textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80", + textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1", button: "h-8 px-2 text-sm bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50", icon: "h-4", + iconSpacing: "gap-x-0.5", shortcutVariant: "medium" as const, shortcut: "ml-1.5 -mr-0.5 border-dimmed/40 text-dimmed group-hover:border-dimmed group-hover:text-bright", }, "tertiary/medium": { - textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80", + textColor: "text-dimmed group-hover:text-bright transition group-disabled:text-dimmed/80 px-1", button: "h-8 px-2 text-sm bg-transparent group-hover:bg-slate-850 disabled:opacity-50", icon: "h-4", + iconSpacing: "gap-x-0.5", shortcutVariant: "medium" as const, shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-dimmed group-hover:border-bright/60 group-hover:text-bright", }, "danger/medium": { - textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80", + textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/80 px-1", button: "h-8 px-2 text-sm bg-rose-600 group-hover:bg-rose-500 disabled:opacity-50", icon: "h-4", + iconSpacing: "gap-x-0.5", shortcutVariant: "medium" as const, shortcut: "ml-1.5 -mr-0.5 border-bright/40 text-bright group-hover:border-bright/60", }, "primary/large": { - textColor: "text-bright group-hover:text-white transition group-disabled:text-dimmed/80", + textColor: "text-bright group-hover:text-white transition group-disabled:text-dimmed/80 px-1", button: - "h-10 px-2 text-sm font-medium bg-indigo-600 group-hover:bg-indigo-500/90 disabled:opacity-50", + "h-10 px-2 text-sm font-medium bg-indigo-600 group-hover:bg-indigo-500/90 group-disabled:opacity-50", icon: "h-5", + iconSpacing: "gap-x-0.5", shortcutVariant: undefined, shortcut: undefined, }, "secondary/large": { - textColor: "text-dimmed", + textColor: "text-dimmed px-1", button: "h-10 px-2 text-sm text-dimmed group-hover:text-bright transition font-medium bg-slate-800 group-hover:bg-slate-700/70 disabled:opacity-50", icon: "h-5", + iconSpacing: "gap-x-0.5", shortcutVariant: undefined, shortcut: undefined, }, "danger/large": { - textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50", + textColor: "text-bright group-hover:text-white transition group-disabled:text-bright/50 px-1", 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", + iconSpacing: "gap-x-0.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", + textColor: "text-bright px-1", button: "h-9 px-[0.475rem] text-sm rounded-sm bg-transparent group-hover:bg-slate-800 transition", icon: "h-5", + iconSpacing: "gap-x-0.5", + shortcutVariant: undefined, + shortcut: undefined, + }, + "small-menu-item": { + textColor: "text-bright", + button: + "h-[1.8rem] px-[0.4rem] text-2sm rounded-sm text-dimmed bg-transparent group-hover:bg-slate-850 transition", + icon: "h-4", + iconSpacing: "gap-x-1.5", + shortcutVariant: undefined, + shortcut: undefined, + }, + "small-menu-sub-item": { + textColor: "text-dimmed", + button: + "h-[1.8rem] px-[0.5rem] ml-5 text-2sm rounded-sm text-dimmed bg-transparent group-hover:bg-slate-850 transition", + icon: undefined, + iconSpacing: undefined, shortcutVariant: undefined, shortcut: undefined, }, @@ -110,7 +140,7 @@ const allVariants = { variant: variant, }; -type ButtonContentPropsType = { +export type ButtonContentPropsType = { children?: React.ReactNode; LeadingIcon?: React.ComponentType | IconNamesOrString; TrailingIcon?: React.ComponentType | IconNamesOrString; @@ -140,6 +170,7 @@ export function ButtonContent(props: ButtonContentPropsType) { // Based on the size prop, we'll use the corresponding variant classnames const btnClassName = cn(allVariants.$all, variation.button); const iconClassName = variation.icon; + const iconSpacingClassName = variation.iconSpacing; const shortcutClassName = variation.shortcut; const textColorClassName = variation.textColor; @@ -148,7 +179,8 @@ export function ButtonContent(props: ButtonContentPropsType) {
{LeadingIcon && @@ -170,7 +202,7 @@ export function ButtonContent(props: ButtonContentPropsType) { {text && (typeof text === "string" ? ( - + {text} ) : ( diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx index fd6cabc6c..e84c64863 100644 --- a/apps/webapp/app/components/primitives/Dialog.tsx +++ b/apps/webapp/app/components/primitives/Dialog.tsx @@ -43,12 +43,12 @@ const DialogContent = React.forwardRef< -
+
{children}
@@ -73,7 +73,7 @@ const DialogContent = React.forwardRef< DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
+
); DialogHeader.displayName = "DialogHeader"; diff --git a/apps/webapp/app/components/primitives/FormTitle.tsx b/apps/webapp/app/components/primitives/FormTitle.tsx index 02a33ae16..98b8c635b 100644 --- a/apps/webapp/app/components/primitives/FormTitle.tsx +++ b/apps/webapp/app/components/primitives/FormTitle.tsx @@ -21,7 +21,7 @@ export function FormTitle({
diff --git a/apps/webapp/app/components/primitives/Help.tsx b/apps/webapp/app/components/primitives/Help.tsx index da126acba..816b2e3f7 100644 --- a/apps/webapp/app/components/primitives/Help.tsx +++ b/apps/webapp/app/components/primitives/Help.tsx @@ -88,12 +88,7 @@ export function HelpContent({ )}
-
+
{children}
diff --git a/apps/webapp/app/components/primitives/NamedIcon.tsx b/apps/webapp/app/components/primitives/NamedIcon.tsx index 523fade79..d776a1d88 100644 --- a/apps/webapp/app/components/primitives/NamedIcon.tsx +++ b/apps/webapp/app/components/primitives/NamedIcon.tsx @@ -59,16 +59,18 @@ import { import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons"; import { ActivityIcon, HourglassIcon } from "lucide-react"; import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon"; +import { EndpointIcon } from "~/assets/icons/EndpointIcon"; import { ErrorIcon } from "~/assets/icons/ErrorIcon"; +import { OneTreeIcon } from "~/assets/icons/OneTreeIcon"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; +import { SaplingIcon } from "~/assets/icons/SaplingIcon"; import { ScheduleIcon } from "~/assets/icons/ScheduleIcon"; +import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon"; import { WebhookIcon } from "~/assets/icons/WebhookIcon"; import { cn } from "~/utils/cn"; +import { tablerIcons } from "~/utils/tablerIcons"; import { LogoIcon } from "../LogoIcon"; import { Spinner } from "./Spinner"; -import { SaplingIcon } from "~/assets/icons/SaplingIcon"; -import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon"; -import { OneTreeIcon } from "~/assets/icons/OneTreeIcon"; -import { tablerIcons } from "~/utils/tablerIcons"; import tablerSpritePath from "./tabler-sprite.svg"; const icons = { @@ -127,7 +129,9 @@ const icons = { "invite-member": (className: string) => ( ), - job: (className: string) => , + job: (className: string) => ( + + ), key: (className: string) => , lightbulb: (className: string) => , "clipboard-checked": (className: string) => ( @@ -183,6 +187,11 @@ const icons = { ), webhook: (className: string) => , + endpoint: (className: string) => , + "http-endpoint": (className: string) => ( + + ), + runs: (className: string) => , }; export type IconNames = keyof typeof icons; diff --git a/apps/webapp/app/components/primitives/PageHeader.tsx b/apps/webapp/app/components/primitives/PageHeader.tsx index e3fab7ed7..e1ff127b1 100644 --- a/apps/webapp/app/components/primitives/PageHeader.tsx +++ b/apps/webapp/app/components/primitives/PageHeader.tsx @@ -6,6 +6,9 @@ import { Paragraph } from "./Paragraph"; import { cn } from "~/utils/cn"; import { NamedIcon } from "./NamedIcon"; import { Tabs, TabsProps } from "./Tabs"; +import { Icon, RenderIcon } from "./Icon"; +import { Button, LinkButton } from "./Buttons"; +import { ArrowUpRightIcon } from "@heroicons/react/20/solid"; type WithChildren = { children: React.ReactNode; @@ -13,7 +16,7 @@ type WithChildren = { export function PageHeader({ children, hideBorder }: WithChildren & { hideBorder?: boolean }) { return ( -
+
{children}
); @@ -24,6 +27,7 @@ export function PageTitleRow({ children }: WithChildren) { } type PageTitleProps = { + icon?: RenderIcon; title: string; backButton?: { to: string; @@ -31,7 +35,7 @@ type PageTitleProps = { }; }; -export function PageTitle({ title, backButton }: PageTitleProps) { +export function PageTitle({ icon, title, backButton }: PageTitleProps) { return (
{backButton && ( @@ -48,7 +52,9 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
)} - {title} + + {icon && } {title} +
); } @@ -89,6 +95,28 @@ export function PageInfoProperty({ icon, label, value, + to, +}: { + icon?: string | React.ReactNode; + label?: string; + value: React.ReactNode; + to?: string; +}) { + if (to === undefined) { + return ; + } + + return ( + + + + ); +} + +function PageInfoPropertyContent({ + icon, + label, + value, }: { icon?: string | React.ReactNode; label?: string; diff --git a/apps/webapp/app/components/primitives/Paragraph.tsx b/apps/webapp/app/components/primitives/Paragraph.tsx index 3abaaaded..8a3b6b386 100644 --- a/apps/webapp/app/components/primitives/Paragraph.tsx +++ b/apps/webapp/app/components/primitives/Paragraph.tsx @@ -51,6 +51,7 @@ const paragraphVariants = { text: "font-sans text-xxs font-normal text-bright", spacing: "mb-1", }, + "extra-extra-small/caps": { text: "font-sans text-xxs uppercase tracking-wider font-normal text-dimmed", spacing: "mb-1", @@ -59,6 +60,10 @@ const paragraphVariants = { text: "font-sans text-xxs uppercase tracking-wider font-normal text-bright", spacing: "mb-1", }, + "extra-extra-small/dimmed/caps": { + text: "font-sans text-xxs uppercase tracking-wider font-normal text-dimmed", + spacing: "mb-1", + }, }; export type ParagraphVariant = keyof typeof paragraphVariants; diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 357b1c54e..cb374e75f 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -3,12 +3,11 @@ import * as React from "react"; import * as PopoverPrimitive from "@radix-ui/react-popover"; import { cn } from "~/utils/cn"; -import { Paragraph } from "./Paragraph"; +import { Paragraph, ParagraphVariant } from "./Paragraph"; import { ChevronDownIcon, EllipsisVerticalIcon } from "@heroicons/react/24/solid"; -import { LinkButton } from "./Buttons"; +import { ButtonContentPropsType, LinkButton } from "./Buttons"; const Popover = PopoverPrimitive.Root; - const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< @@ -22,19 +21,27 @@ const PopoverContent = React.forwardRef< sideOffset={sideOffset} avoidCollisions={true} className={cn( - "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + "z-50 min-w-max rounded-md border bg-midnight-850 p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className )} - style={{ maxHeight: "var(--radix-popover-content-available-height)" }} + style={{ + maxHeight: "var(--radix-popover-content-available-height)", + }} {...props} /> )); PopoverContent.displayName = PopoverPrimitive.Content.displayName; -function PopoverSectionHeader({ title }: { title: string }) { +function PopoverSectionHeader({ + title, + variant = "extra-extra-small/dimmed/caps", +}: { + title: string; + variant?: ParagraphVariant; +}) { return ( - + {title} ); @@ -45,28 +52,33 @@ function PopoverMenuItem({ icon, title, isSelected, + variant = { variant: "small-menu-item" }, + leadingIconClassName, }: { to: string; - icon: string; + icon: string | React.ComponentType; title: React.ReactNode; isSelected?: boolean; + variant?: ButtonContentPropsType; + leadingIconClassName?: string; }) { return ( {title} ); } -function PopoverArrowTrigger({ +function PopoverCustomTrigger({ isOpen, children, className, @@ -76,15 +88,50 @@ function PopoverArrowTrigger({ - + {children} + + ); +} + +function PopoverArrowTrigger({ + isOpen, + children, + fullWidth = false, + overflowHidden = false, + className, + ...props +}: { + isOpen?: boolean; + fullWidth?: boolean; + overflowHidden?: boolean; +} & React.ComponentPropsWithoutRef) { + return ( + + {children} ); @@ -113,7 +160,8 @@ export { PopoverTrigger, PopoverContent, PopoverSectionHeader, + PopoverCustomTrigger, PopoverArrowTrigger, - PopoverMenuItem, PopoverVerticalEllipseTrigger, + PopoverMenuItem, }; diff --git a/apps/webapp/app/components/primitives/Sheet.tsx b/apps/webapp/app/components/primitives/Sheet.tsx index 5432f8007..844c29ca1 100644 --- a/apps/webapp/app/components/primitives/Sheet.tsx +++ b/apps/webapp/app/components/primitives/Sheet.tsx @@ -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-uiBorder", + "fixed z-50 scale-100 gap-4 bg-midnight-900 shadow-lg shadow-white/10 opacity-100 border-l border-ui-border", { variants: { position: { @@ -154,7 +154,7 @@ const SheetContent = React.forwardRef< {...props} >
-
+
Close @@ -181,7 +181,7 @@ export const SheetBody = ({ className, ...props }: React.HTMLAttributes) => (
) => (
-
{children}
+
{children}
); diff --git a/apps/webapp/app/components/primitives/Spinner.tsx b/apps/webapp/app/components/primitives/Spinner.tsx index fbe430848..b8593287c 100644 --- a/apps/webapp/app/components/primitives/Spinner.tsx +++ b/apps/webapp/app/components/primitives/Spinner.tsx @@ -5,7 +5,7 @@ export function Spinner({ color = "blue", }: { className?: string; - color?: "blue" | "white"; + color?: "blue" | "white" | "muted"; }) { const colors = { blue: { @@ -16,6 +16,10 @@ export function Spinner({ light: "rgba(255, 255, 255, 0.4)", dark: "rgba(255, 255, 255)", }, + muted: { + light: "#1C2433", + dark: "#3C4B62", + }, }; const currentColor = colors[color]; diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index c7240256a..9c02814d8 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -16,7 +16,7 @@ export const Table = forwardRef( return (
return (
{children} @@ -55,7 +55,7 @@ type TableBodyProps = { export const TableBody = forwardRef( ({ className, children }, ref) => { return ( - + {children} ); @@ -106,7 +106,7 @@ export const TableHeaderCell = forwardRef +
{tabs.map((tab, index) => ( {({ isActive, isPending }) => ( diff --git a/apps/webapp/app/components/run/RunOverview.tsx b/apps/webapp/app/components/run/RunOverview.tsx index 130ecfd0d..96f15f53a 100644 --- a/apps/webapp/app/components/run/RunOverview.tsx +++ b/apps/webapp/app/components/run/RunOverview.tsx @@ -1,6 +1,7 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid"; +import { PlayIcon } from "@heroicons/react/20/solid"; +import { BoltIcon } from "@heroicons/react/24/solid"; import { Form, Outlet, @@ -262,7 +263,9 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps ) : ( run.output === null && ( - This Run returned nothing. + + This Run returned nothing. + ) )} @@ -273,7 +276,7 @@ export function RunOverview({ run, trigger, showRerun, paths }: RunOverviewProps {/* Detail view */}
- Detail + Details {selectedId ? : Select a task or trigger}
@@ -338,50 +341,52 @@ function RerunPopover({ Rerun Job - + {environmentType === "PRODUCTION" && ( - - This will rerun this Job in your Production environment. - +
+ + This will rerun this Job in your Production environment. + +
)} -
-
+
+
+ + Start a brand new Job run with the same Trigger data as this one. This will re-do + every Task. + - - - Start a brand new job run with the same Trigger data as this one. This will re-do - every task. -
{status === "FAILED" && ( -
+
+ + Continue running this Job run from where it left off. This will skip any Task that + has already been completed. + - - - Continue running this job run from where it left off. This will skip any task that - has already been completed. -
)}
diff --git a/apps/webapp/app/components/runs/RunsTable.tsx b/apps/webapp/app/components/runs/RunsTable.tsx index 7f185e3c3..77aabc6db 100644 --- a/apps/webapp/app/components/runs/RunsTable.tsx +++ b/apps/webapp/app/components/runs/RunsTable.tsx @@ -129,7 +129,7 @@ export function RunsTable({ function NoRuns({ title }: { title: string }) { return (
- + {title}
diff --git a/apps/webapp/app/hooks/useFilterJobs.ts b/apps/webapp/app/hooks/useFilterJobs.ts index 950e19a43..9d220952e 100644 --- a/apps/webapp/app/hooks/useFilterJobs.ts +++ b/apps/webapp/app/hooks/useFilterJobs.ts @@ -1,4 +1,4 @@ -import { ProjectJob } from "./useJobs"; +import { ProjectJob } from "~/presenters/JobListPresenter.server"; import { useTextFilter } from "./useTextFilter"; import { useToggleFilter } from "./useToggleFilter"; diff --git a/apps/webapp/app/hooks/useIntegrationClient.tsx b/apps/webapp/app/hooks/useIntegrationClient.tsx index e7c5d0b5f..bfe9fad24 100644 --- a/apps/webapp/app/hooks/useIntegrationClient.tsx +++ b/apps/webapp/app/hooks/useIntegrationClient.tsx @@ -1,14 +1,14 @@ import { UIMatch } from "@remix-run/react"; import { UseDataFunctionReturn } from "remix-typedjson"; import invariant from "tiny-invariant"; -import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route"; +import type { loader } from "~/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route"; import { useTypedMatchesData } from "./useTypedMatchData"; export type MatchedClient = UseDataFunctionReturn["client"]; export function useOptionalIntegrationClient(matches?: UIMatch[]) { const routeMatch = useTypedMatchesData({ - id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam", + id: "routes/_app.orgs.$organizationSlug.integrations_.$clientParam", matches, }); diff --git a/apps/webapp/app/hooks/useIsProjectChildPage.ts b/apps/webapp/app/hooks/useIsOrgChildPage.ts similarity index 69% rename from apps/webapp/app/hooks/useIsProjectChildPage.ts rename to apps/webapp/app/hooks/useIsOrgChildPage.ts index cd9cf2f8a..685630ccc 100644 --- a/apps/webapp/app/hooks/useIsProjectChildPage.ts +++ b/apps/webapp/app/hooks/useIsOrgChildPage.ts @@ -1,11 +1,11 @@ import { UIMatch, useMatches } from "@remix-run/react"; -export function useIsProjectChildPage(matches?: UIMatch[]) { +export function useIsOrgChildPage(matches?: UIMatch[]) { if (!matches) { matches = useMatches(); } return matches.some((matchData) => { - return matchData.id.startsWith("routes/_app.orgs.$organizationSlug.projects.$projectParam"); + return matchData.id.startsWith("routes/_app.orgs.$organizationSlug"); }); } diff --git a/apps/webapp/app/hooks/useJob.tsx b/apps/webapp/app/hooks/useJob.tsx index 32c6e682e..7064e8f30 100644 --- a/apps/webapp/app/hooks/useJob.tsx +++ b/apps/webapp/app/hooks/useJob.tsx @@ -9,17 +9,18 @@ export type MatchedJob = UseDataFunctionReturn["job"]; export const jobMatchId = "routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam"; + export function useOptionalJob(matches?: UIMatch[]) { const routeMatch = useTypedMatchesData({ id: jobMatchId, matches, }); - if (!routeMatch || !routeMatch.job) { + if (!routeMatch) { return undefined; } - return routeMatch.projectJobs.find((j) => j.id === routeMatch.job.id); + return routeMatch.job; } export function useJob(matches?: UIMatch[]) { diff --git a/apps/webapp/app/hooks/useJobs.tsx b/apps/webapp/app/hooks/useJobs.tsx deleted file mode 100644 index 8c9821cc4..000000000 --- a/apps/webapp/app/hooks/useJobs.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { UseDataFunctionReturn } from "remix-typedjson"; -import invariant from "tiny-invariant"; -import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route"; -import { UIMatch } from "@remix-run/react"; -import { useTypedMatchesData } from "./useTypedMatchData"; - -export type ProjectJob = UseDataFunctionReturn["projectJobs"][number]; - -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?: UIMatch[]) { - const routeMatch = useTypedMatchesData({ - id: jobsMatchId, - matches, - }); - - return routeMatch?.projectJobs; -} - -export function useJobs(matches?: UIMatch[]) { - const jobs = useOptionalJobs(matches); - invariant(jobs, "Jobs must be defined"); - return jobs; -} diff --git a/apps/webapp/app/hooks/useOrganizations.ts b/apps/webapp/app/hooks/useOrganizations.ts index 35a79d335..ace3e5da5 100644 --- a/apps/webapp/app/hooks/useOrganizations.ts +++ b/apps/webapp/app/hooks/useOrganizations.ts @@ -1,17 +1,17 @@ import { UseDataFunctionReturn, useTypedRouteLoaderData } from "remix-typedjson"; import invariant from "tiny-invariant"; import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route"; -import type { loader as appLoader } from "~/routes/_app/route"; import { hydrateObject, useMatchesData } from "~/utils"; import { useChanged } from "./useChanged"; import { UIMatch } from "@remix-run/react"; import { useTypedMatchesData } from "./useTypedMatchData"; -export type MatchedOrganization = UseDataFunctionReturn["organizations"][number]; +export type MatchedOrganization = UseDataFunctionReturn["organizations"][number]; +export const organizationMatchId = "routes/_app.orgs.$organizationSlug"; export function useOptionalOrganizations(matches?: UIMatch[]) { - const data = useTypedMatchesData({ - id: "routes/_app", + const data = useTypedMatchesData({ + id: "routes/_app.orgs.$organizationSlug", matches, }); return data?.organizations; diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index df0475fbe..07b46b1d2 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -9,61 +9,12 @@ import { customAlphabet } from "nanoid"; import slug from "slug"; import { prisma, PrismaClientOrTransaction } from "~/db.server"; import { createProject } from "./project.server"; +import { generate } from "random-words"; import { createApiKeyForEnv, createPkApiKeyForEnv, envSlug } from "./api-key.server"; export type { Organization }; const nanoid = customAlphabet("1234567890abcdef", 4); -const apiKeyId = customAlphabet( - "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", - 12 -); - -export function getOrganizationFromSlug({ - userId, - slug, -}: Pick & { - userId: User["id"]; -}) { - return prisma.organization.findFirst({ - include: { - environments: true, - }, - where: { slug, members: { some: { userId } } }, - }); -} - -export function getOrganizations({ userId }: { userId: User["id"] }) { - return prisma.organization.findMany({ - where: { members: { some: { userId } } }, - orderBy: { createdAt: "desc" }, - include: { - environments: { - orderBy: { slug: "asc" }, - }, - projects: { - orderBy: { name: "asc" }, - include: { - _count: { - select: { - jobs: { - where: { - internal: false, - deletedAt: null, - }, - }, - }, - }, - }, - }, - _count: { - select: { - members: true, - }, - }, - }, - }); -} export async function createOrganization( { @@ -136,12 +87,14 @@ export async function createEnvironment( const slug = envSlug(type); const apiKey = createApiKeyForEnv(type); const pkApiKey = createPkApiKeyForEnv(type); + const shortcode = createShortcode().join("-"); return await prismaClient.runtimeEnvironment.create({ data: { slug, apiKey, pkApiKey, + shortcode, autoEnableInternalSources: type !== "DEVELOPMENT", organization: { connect: { @@ -158,3 +111,7 @@ export async function createEnvironment( }, }); } + +function createShortcode() { + return generate({ exactly: 2 }); +} diff --git a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts b/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts index bd5f4ef87..23f803926 100644 --- a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts +++ b/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts @@ -14,6 +14,7 @@ import { IndexEndpointStats, parseEndpointIndexStats, } from "@trigger.dev/core"; +import { sortEnvironments } from "~/services/environmentSort.server"; export type Client = { slug: string; @@ -197,31 +198,20 @@ export class EnvironmentsPresenter { } return { - environments: filtered - .map((environment) => ({ + environments: sortEnvironments( + filtered.map((environment) => ({ id: environment.id, apiKey: environment.apiKey, pkApiKey: environment.pkApiKey, type: environment.type, slug: environment.slug, })) - .sort((a, b) => { - const aIndex = environmentSortOrder.indexOf(a.type); - const bIndex = environmentSortOrder.indexOf(b.type); - return aIndex - bIndex; - }), + ), clients, }; } } -const environmentSortOrder: RuntimeEnvironmentType[] = [ - "DEVELOPMENT", - "PREVIEW", - "STAGING", - "PRODUCTION", -]; - function endpointClient( endpoint: Pick & { indexings: Pick[]; diff --git a/apps/webapp/app/presenters/HttpEndpointPresenter.server.ts b/apps/webapp/app/presenters/HttpEndpointPresenter.server.ts new file mode 100644 index 000000000..fe792483c --- /dev/null +++ b/apps/webapp/app/presenters/HttpEndpointPresenter.server.ts @@ -0,0 +1,154 @@ +import { TriggerHttpEndpoint } from "@trigger.dev/database"; +import { z } from "zod"; +import { PrismaClient, prisma } from "~/db.server"; +import { Project } from "~/models/project.server"; +import { User } from "~/models/user.server"; +import { sortEnvironments } from "~/services/environmentSort.server"; +import { httpEndpointUrl } from "~/services/httpendpoint/HandleHttpEndpointService"; +import { getSecretStore } from "~/services/secrets/secretStore.server"; + +export class HttpEndpointPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ + userId, + projectSlug, + httpEndpointKey, + }: { + userId: string; + projectSlug: string; + httpEndpointKey: string; + }) { + const httpEndpoint = await this.#prismaClient.triggerHttpEndpoint.findFirst({ + select: { + id: true, + key: true, + icon: true, + title: true, + updatedAt: true, + projectId: true, + secretReference: { + select: { + key: true, + provider: true, + }, + }, + httpEndpointEnvironments: { + select: { + id: true, + immediateResponseFilter: true, + skipTriggeringRuns: true, + source: true, + active: true, + updatedAt: true, + environment: { + select: { + type: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }, + }, + where: { + key: httpEndpointKey, + project: { + slug: projectSlug, + organization: { + members: { + some: { + userId, + }, + }, + }, + }, + }, + }); + + if (!httpEndpoint) { + throw new Error("Could not find http endpoint"); + } + + const environments = await this.#prismaClient.runtimeEnvironment.findMany({ + select: { + id: true, + type: true, + slug: true, + shortcode: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + where: { + projectId: httpEndpoint.projectId, + }, + }); + + const relevantEnvironments = sortEnvironments( + environments + .filter( + (environment) => environment.orgMember === null || environment.orgMember.userId === userId + ) + .map((environment) => ({ + ...environment, + webhookUrl: httpEndpointUrl({ httpEndpointId: httpEndpoint.id, environment }), + })) + ); + + //get the secret + const secretStore = getSecretStore(httpEndpoint.secretReference.provider); + let secret: string | undefined; + try { + const secretData = await secretStore.getSecretOrThrow( + z.object({ secret: z.string() }), + httpEndpoint.secretReference.key + ); + secret = secretData.secret; + } catch (e) { + let error = e instanceof Error ? e.message : JSON.stringify(e); + throw new Error(`Could not retrieve secret: ${error}`); + } + if (!secret) { + throw new Error("Could not find secret"); + } + + const httpEndpointEnvironments = httpEndpoint.httpEndpointEnvironments + .filter( + (httpEndpointEnvironment) => + httpEndpointEnvironment.environment.orgMember === null || + httpEndpointEnvironment.environment.orgMember.userId === userId + ) + .map((endpointEnv) => ({ + ...endpointEnv, + immediateResponseFilter: endpointEnv.immediateResponseFilter != null, + environment: { + type: endpointEnv.environment.type, + }, + webhookUrl: relevantEnvironments.find((e) => e.type === endpointEnv.environment.type) + ?.webhookUrl, + })); + + return { + httpEndpoint: { + ...httpEndpoint, + + httpEndpointEnvironments, + }, + environments: relevantEnvironments, + unconfiguredEnvironments: relevantEnvironments.filter( + (e) => httpEndpointEnvironments.find((h) => h.environment.type === e.type) === undefined + ), + secret, + }; + } +} diff --git a/apps/webapp/app/presenters/HttpEndpointsPresenter.server.ts b/apps/webapp/app/presenters/HttpEndpointsPresenter.server.ts new file mode 100644 index 000000000..8f4d3134f --- /dev/null +++ b/apps/webapp/app/presenters/HttpEndpointsPresenter.server.ts @@ -0,0 +1,64 @@ +import { PrismaClient, prisma } from "~/db.server"; +import { Project } from "~/models/project.server"; +import { User } from "~/models/user.server"; + +export class HttpEndpointsPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ + userId, + slug, + }: Pick & { + userId: User["id"]; + }) { + const httpEndpoints = await this.#prismaClient.triggerHttpEndpoint.findMany({ + select: { + id: true, + key: true, + icon: true, + title: true, + updatedAt: true, + httpEndpointEnvironments: { + select: { + id: true, + environment: { + select: { + type: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }, + }, + where: { + project: { + slug, + organization: { + members: { + some: { + userId, + }, + }, + }, + }, + }, + }); + + return httpEndpoints.map((httpEndpoint) => ({ + ...httpEndpoint, + httpEndpointEnvironments: httpEndpoint.httpEndpointEnvironments.filter( + (httpEndpointEnvironment) => + httpEndpointEnvironment.environment.orgMember === null || + httpEndpointEnvironment.environment.orgMember.userId === userId + ), + })); + } +} diff --git a/apps/webapp/app/presenters/IntegrationClientConnectionsPresenter.server.ts b/apps/webapp/app/presenters/IntegrationClientConnectionsPresenter.server.ts index f1438ea3f..3e02167ec 100644 --- a/apps/webapp/app/presenters/IntegrationClientConnectionsPresenter.server.ts +++ b/apps/webapp/app/presenters/IntegrationClientConnectionsPresenter.server.ts @@ -1,7 +1,6 @@ import { User } from "@trigger.dev/database"; import { PrismaClient, prisma } from "~/db.server"; import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; import { ConnectionMetadataSchema } from "~/services/externalApis/types"; export class IntegrationClientConnectionsPresenter { @@ -14,12 +13,10 @@ export class IntegrationClientConnectionsPresenter { public async call({ userId, organizationSlug, - projectSlug, clientSlug, }: { userId: User["id"]; organizationSlug: Organization["slug"]; - projectSlug: Project["slug"]; clientSlug: string; }) { const connections = await this.#prismaClient.integrationConnection.findMany({ diff --git a/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts b/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts index 901d79b17..5d65ac668 100644 --- a/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts +++ b/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts @@ -17,12 +17,10 @@ export class IntegrationClientPresenter { public async call({ userId, organizationSlug, - projectSlug, clientSlug, }: { userId: User["id"]; organizationSlug: Organization["slug"]; - projectSlug: Project["slug"]; clientSlug: string; }) { const integration = await this.#prismaClient.integration.findFirst({ @@ -59,8 +57,8 @@ export class IntegrationClientPresenter { jobIntegrations: { where: { job: { - project: { - slug: projectSlug, + organization: { + slug: organizationSlug, }, internal: false, deletedAt: null, diff --git a/apps/webapp/app/presenters/IntegrationClientScopesPresenter.server.ts b/apps/webapp/app/presenters/IntegrationClientScopesPresenter.server.ts index 4687ee0f9..25bbc783a 100644 --- a/apps/webapp/app/presenters/IntegrationClientScopesPresenter.server.ts +++ b/apps/webapp/app/presenters/IntegrationClientScopesPresenter.server.ts @@ -15,12 +15,10 @@ export class IntegrationClientScopesPresenter { public async call({ userId, organizationSlug, - projectSlug, clientSlug, }: { userId: User["id"]; organizationSlug: Organization["slug"]; - projectSlug: Project["slug"]; clientSlug: string; }) { const integration = await this.#prismaClient.integration.findFirst({ diff --git a/apps/webapp/app/presenters/IntegrationsPresenter.server.ts b/apps/webapp/app/presenters/IntegrationsPresenter.server.ts index c38449a17..57e477ca6 100644 --- a/apps/webapp/app/presenters/IntegrationsPresenter.server.ts +++ b/apps/webapp/app/presenters/IntegrationsPresenter.server.ts @@ -25,11 +25,9 @@ export class IntegrationsPresenter { public async call({ userId, - projectSlug, organizationSlug, }: { userId: User["id"]; - projectSlug: Project["slug"]; organizationSlug: Organization["slug"]; }) { const clients = await this.#prismaClient.integration.findMany({ @@ -67,8 +65,8 @@ export class IntegrationsPresenter { jobIntegrations: { where: { job: { - project: { - slug: projectSlug, + organization: { + slug: organizationSlug, }, internal: false, deletedAt: null, diff --git a/apps/webapp/app/presenters/JobListPresenter.server.ts b/apps/webapp/app/presenters/JobListPresenter.server.ts index 3c997c646..587d60481 100644 --- a/apps/webapp/app/presenters/JobListPresenter.server.ts +++ b/apps/webapp/app/presenters/JobListPresenter.server.ts @@ -8,6 +8,9 @@ import { Organization } from "~/models/organization.server"; import { Project } from "~/models/project.server"; import { User } from "~/models/user.server"; import { z } from "zod"; +import { projectPath } from "~/utils/pathBuilder"; + +export type ProjectJob = Awaited>[0]; export class JobListPresenter { #prismaClient: PrismaClient; @@ -23,8 +26,8 @@ export class JobListPresenter { integrationSlug, }: { userId: User["id"]; - projectSlug: Project["slug"]; - organizationSlug?: Organization["slug"]; + projectSlug?: Project["slug"]; + organizationSlug: Organization["slug"]; integrationSlug?: string; }) { const orgWhere: Prisma.JobWhereInput["organization"] = organizationSlug @@ -68,6 +71,8 @@ export class JobListPresenter { }, }, }, + triggerLink: true, + triggerHelp: true, }, }, environment: { @@ -90,14 +95,21 @@ export class JobListPresenter { type: true, }, }, + project: { + select: { + slug: true, + }, + }, }, where: { internal: false, deletedAt: null, organization: orgWhere, - project: { - slug: projectSlug, - }, + project: projectSlug + ? { + slug: projectSlug, + } + : undefined, integrations: integrationsWhere, }, orderBy: [{ title: "asc" }], @@ -182,6 +194,11 @@ export class JobListPresenter { title: eventSpecification.title, icon: eventSpecification.icon, source: eventSpecification.source, + link: projectSlug + ? `${projectPath({ slug: organizationSlug }, { slug: projectSlug })}/${ + alias.version.triggerLink + }` + : undefined, }, integrations, hasIntegrationsRequiringAction: integrations.some( diff --git a/apps/webapp/app/presenters/JobPresenter.server.ts b/apps/webapp/app/presenters/JobPresenter.server.ts new file mode 100644 index 000000000..82c7c7654 --- /dev/null +++ b/apps/webapp/app/presenters/JobPresenter.server.ts @@ -0,0 +1,234 @@ +import { + DisplayProperty, + DisplayPropertySchema, + EventSpecificationSchema, + TriggerHelpSchema, +} from "@trigger.dev/core"; +import { PrismaClient, Prisma, prisma } from "~/db.server"; +import { Organization } from "~/models/organization.server"; +import { Project } from "~/models/project.server"; +import { User } from "~/models/user.server"; +import { z } from "zod"; +import { projectPath } from "~/utils/pathBuilder"; +import { Job } from "@trigger.dev/database"; + +export class JobPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ + userId, + jobSlug, + projectSlug, + organizationSlug, + }: { + userId: User["id"]; + jobSlug: Job["slug"]; + projectSlug: Project["slug"]; + organizationSlug: Organization["slug"]; + }) { + const job = await this.#prismaClient.job.findFirst({ + select: { + id: true, + slug: true, + title: true, + aliases: { + select: { + version: { + select: { + version: true, + eventSpecification: true, + properties: true, + status: true, + runs: { + select: { + createdAt: true, + status: true, + }, + take: 1, + orderBy: [{ createdAt: "desc" }], + }, + integrations: { + select: { + key: true, + integration: { + select: { + slug: true, + definition: true, + setupStatus: true, + }, + }, + }, + }, + triggerLink: true, + triggerHelp: true, + }, + }, + environment: { + select: { + type: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + where: { + name: "latest", + }, + }, + dynamicTriggers: { + select: { + type: true, + }, + }, + project: { + select: { + slug: true, + }, + }, + _count: { + select: { + runs: { + where: { + isTest: false, + }, + }, + }, + }, + }, + where: { + slug: jobSlug, + deletedAt: null, + organization: { + members: { + some: { + userId, + }, + }, + }, + project: { + slug: projectSlug, + }, + }, + }); + + if (!job) { + return undefined; + } + + //the best alias to select: + // 1. Logged-in user dev + // 2. Prod + // 3. Any other user's dev + const sortedAliases = job.aliases.sort((a, b) => { + if (a.environment.type === "DEVELOPMENT" && a.environment.orgMember?.userId === userId) { + return -1; + } + + if (b.environment.type === "DEVELOPMENT" && b.environment.orgMember?.userId === userId) { + return 1; + } + + if (a.environment.type === "PRODUCTION") { + return -1; + } + + if (b.environment.type === "PRODUCTION") { + return 1; + } + + return 0; + }); + + const alias = sortedAliases.at(0); + + if (!alias) { + throw new Error(`No aliases found for job ${job.id}, this should never happen.`); + } + + const eventSpecification = EventSpecificationSchema.parse(alias.version.eventSpecification); + + const lastRuns = job.aliases + .map((alias) => alias.version.runs.at(0)) + .filter(Boolean) + .sort((a, b) => { + return b.createdAt.getTime() - a.createdAt.getTime(); + }); + + const lastRun = lastRuns.at(0); + + const integrations = alias.version.integrations.map((integration) => ({ + key: integration.key, + title: integration.integration.slug, + icon: integration.integration.definition.icon ?? integration.integration.definition.id, + setupStatus: integration.integration.setupStatus, + })); + + let properties: DisplayProperty[] = []; + + if (eventSpecification.properties) { + properties = [...properties, ...eventSpecification.properties]; + } + + if (alias.version.properties) { + const versionProperties = z.array(DisplayPropertySchema).parse(alias.version.properties); + 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, + })); + + const projectRootPath = projectPath({ slug: organizationSlug }, { slug: projectSlug }); + + //we exclude test runs from this count + const hasRealRuns = job._count.runs > 0; + + 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, + icon: eventSpecification.icon, + source: eventSpecification.source, + link: `${projectRootPath}/${alias.version.triggerLink}`, + }, + noRunsHelp: hasRealRuns + ? undefined + : this.#getNoRunsHelp(alias.version.triggerHelp, projectRootPath), + integrations, + hasIntegrationsRequiringAction: integrations.some((i) => i.setupStatus === "MISSING_FIELDS"), + lastRun, + properties, + environments, + }; + } + + #getNoRunsHelp(data: Prisma.JsonValue, projectPath: string) { + const triggerHelp = TriggerHelpSchema.nullish().parse(data); + if (!triggerHelp) { + return undefined; + } + + if (triggerHelp.noRuns) { + triggerHelp.noRuns.link = triggerHelp.noRuns.link + ? `${projectPath}/${triggerHelp.noRuns.link}` + : undefined; + + return triggerHelp.noRuns; + } + } +} diff --git a/apps/webapp/app/presenters/NewOrganizationPresenter.server.ts b/apps/webapp/app/presenters/NewOrganizationPresenter.server.ts new file mode 100644 index 000000000..4d3fe0aae --- /dev/null +++ b/apps/webapp/app/presenters/NewOrganizationPresenter.server.ts @@ -0,0 +1,20 @@ +import { PrismaClient, User } from "@trigger.dev/database"; +import { prisma } from "~/db.server"; + +export class NewOrganizationPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ userId }: { userId: User["id"] }) { + const organizations = await this.#prismaClient.organization.findMany({ + where: { members: { some: { userId } } }, + }); + + return { + hasOrganizations: organizations.length > 0, + }; + } +} diff --git a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts new file mode 100644 index 000000000..d34e93e5c --- /dev/null +++ b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts @@ -0,0 +1,117 @@ +import { PrismaClient } from "@trigger.dev/database"; +import { prisma } from "~/db.server"; +import { getCurrentProjectId } from "~/services/currentProject.server"; +import { ProjectPresenter } from "./ProjectPresenter.server"; + +type Org = Awaited>[number]; + +export class OrganizationsPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ + userId, + organizationSlug, + request, + projectSlug, + }: { + userId: string; + organizationSlug: string; + request: Request; + projectSlug?: string; + }) { + const organizations = await this.getOrganizations(userId); + + const organization = organizations.find((o) => o.slug === organizationSlug); + if (!organization) { + throw new Response("Not Found", { status: 404 }); + } + + const project = await this.getProject(organization, projectSlug, request, userId); + + return { organizations, organization, project }; + } + + async getOrganizations(userId: string) { + const orgs = await this.#prismaClient.organization.findMany({ + where: { members: { some: { userId } } }, + orderBy: { createdAt: "desc" }, + include: { + projects: { + orderBy: { name: "asc" }, + include: { + _count: { + select: { + jobs: { + where: { + internal: false, + deletedAt: null, + }, + }, + sources: { + where: { + active: false, + }, + }, + httpEndpoints: true, + }, + }, + }, + }, + _count: { + select: { + members: true, + integrations: { + where: { + setupStatus: "MISSING_FIELDS", + }, + }, + }, + }, + }, + }); + + return orgs.map((org) => { + return { + id: org.id, + slug: org.slug, + title: org.title, + projects: org.projects.map((project) => ({ + id: project.id, + slug: project.slug, + name: project.name, + jobCount: project._count.jobs, + })), + hasUnconfiguredIntegrations: org._count.integrations > 0, + memberCount: org._count.members, + }; + }); + } + + async getProject( + organization: Org, + projectSlug: string | undefined, + request: Request, + userId: string + ) { + const projectPresenter = new ProjectPresenter(); + + if (!projectSlug) { + const projectId = await getCurrentProjectId(request); + const orgProject = organization.projects.find((p) => p.id === projectId); + if (!orgProject) { + throw new Response("Not Found", { status: 404 }); + } + projectSlug = orgProject.slug; + } + + const project = await projectPresenter.call({ userId, slug: projectSlug }); + if (!project) { + throw new Response("Not Found", { status: 404 }); + } + return project; + } +} diff --git a/apps/webapp/app/presenters/ProjectPresenter.server.ts b/apps/webapp/app/presenters/ProjectPresenter.server.ts index fe55f354a..0d18091c1 100644 --- a/apps/webapp/app/presenters/ProjectPresenter.server.ts +++ b/apps/webapp/app/presenters/ProjectPresenter.server.ts @@ -91,6 +91,13 @@ export class ProjectPresenter { active: false, }, }, + jobs: { + where: { + internal: false, + deletedAt: null, + }, + }, + httpEndpoints: true, }, }, organization: { @@ -135,7 +142,8 @@ export class ProjectPresenter { createdAt: project.createdAt, updatedAt: project.updatedAt, hasInactiveExternalTriggers: project._count.sources > 0, - hasUnconfiguredIntegrations: project.organization._count.integrations > 0, + jobCount: project._count.jobs, + httpEndpointCount: project._count.httpEndpoints, environments: project.environments.map((environment) => ({ id: environment.id, slug: environment.slug, diff --git a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts new file mode 100644 index 000000000..55adbd6cc --- /dev/null +++ b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts @@ -0,0 +1,49 @@ +import { PrismaClient } from "@trigger.dev/database"; +import { prisma } from "~/db.server"; +import { getCurrentProjectId } from "~/services/currentProject.server"; + +export class SelectBestProjectPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ userId, request }: { userId: string; request: Request }) { + //try get current project from cookie + const projectId = await getCurrentProjectId(request); + if (projectId) { + const project = await this.#prismaClient.project.findUnique({ + where: { id: projectId, organization: { members: { some: { userId } } } }, + include: { organization: true }, + }); + if (project) { + return { project, organization: project.organization }; + } + } + + //failing that, we pick the project with the most jobs + const projects = await this.#prismaClient.project.findMany({ + include: { + organization: true, + }, + where: { + organization: { + members: { some: { userId } }, + }, + }, + orderBy: { + jobs: { + _count: "desc", + }, + }, + take: 1, + }); + + if (projects.length === 0) { + throw new Response("Not Found", { status: 404 }); + } + + return { project: projects[0], organization: projects[0].organization }; + } +} diff --git a/apps/webapp/app/routes/_app._index/route.tsx b/apps/webapp/app/routes/_app._index/route.tsx new file mode 100644 index 000000000..466e074b2 --- /dev/null +++ b/apps/webapp/app/routes/_app._index/route.tsx @@ -0,0 +1,19 @@ +import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server"; +import { requireUserId } from "~/services/session.server"; +import { newOrganizationPath, projectPath } from "~/utils/pathBuilder"; + +//this loader chooses the best project to redirect you to, ideally based on the cookie +export const loader = async ({ request }: LoaderFunctionArgs) => { + const userId = await requireUserId(request); + + const presenter = new SelectBestProjectPresenter(); + try { + const { project, organization } = await presenter.call({ userId, request }); + + return redirect(projectPath(organization, project)); + } catch (e) { + //this should only happen if the user has no projects + return redirect(newOrganizationPath()); + } +}; diff --git a/apps/webapp/app/routes/_app._orgaccount._index/OrganizationGrid.tsx b/apps/webapp/app/routes/_app._orgaccount._index/OrganizationGrid.tsx deleted file mode 100644 index 5a7b004ac..000000000 --- a/apps/webapp/app/routes/_app._orgaccount._index/OrganizationGrid.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { ChevronRightIcon } from "@heroicons/react/24/solid"; -import { Link } from "@remix-run/react"; -import simplur from "simplur"; -import { Badge } from "~/components/primitives/Badge"; -import { LinkButton } from "~/components/primitives/Buttons"; -import { Header2 } from "~/components/primitives/Headers"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import type { MatchedOrganization } from "~/hooks/useOrganizations"; -import { cn } from "~/utils/cn"; -import { newProjectPath, organizationPath, projectPath } from "~/utils/pathBuilder"; - -export function OrganizationGridItem({ organization }: { organization: MatchedOrganization }) { - return ( -
  • -
    - -
    -
    -
    -
  • - ); -} diff --git a/apps/webapp/app/routes/_app._orgaccount._index/route.tsx b/apps/webapp/app/routes/_app._orgaccount._index/route.tsx deleted file mode 100644 index 637b97851..000000000 --- a/apps/webapp/app/routes/_app._orgaccount._index/route.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { redirect } from "remix-typedjson"; -import { LinkButton } from "~/components/primitives/Buttons"; -import { useOptionalOrganizations } from "~/hooks/useOrganizations"; -import { getUsersInvites } from "~/models/member.server"; -import { getOrganizations } from "~/models/organization.server"; -import { requireUser } from "~/services/session.server"; -import { invitesPath, newOrganizationPath } from "~/utils/pathBuilder"; -import { OrganizationGridItem } from "./OrganizationGrid"; -import { Link } from "@remix-run/react"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; -import { Paragraph } from "~/components/primitives/Paragraph"; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const user = await requireUser(request); - - //if there are invites then we should redirect to the invites page - const invites = await getUsersInvites({ email: user.email }); - if (invites.length > 0) { - return redirect(invitesPath()); - } - - //if there are no orgs, then redirect to create an org - const organizations = await getOrganizations({ userId: user.id }); - if (organizations.length === 0) { - return redirect(newOrganizationPath()); - } - - return json({}); -}; - -export default function Page() { - const organizations = useOptionalOrganizations(); - - return ( -
      - <> - {organizations && - organizations.map((organization) => ( - - ))} - -
    • - -
      -
      - -
    • -
    - ); -} diff --git a/apps/webapp/app/routes/_app._orgaccount/route.tsx b/apps/webapp/app/routes/_app._orgaccount/route.tsx deleted file mode 100644 index f23f336f5..000000000 --- a/apps/webapp/app/routes/_app._orgaccount/route.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ArrowRightOnRectangleIcon } from "@heroicons/react/20/solid"; -import { Outlet } from "@remix-run/react"; -import { PageContainer, PageBody } from "~/components/layout/AppLayout"; -import { LinkButton } from "~/components/primitives/Buttons"; -import { - PageHeader, - PageTitleRow, - PageTitle, - PageButtons, - PageDescription, - PageTabs, -} from "~/components/primitives/PageHeader"; -import { - newOrganizationPath, - organizationsPath, - accountPath, - logoutPath, -} from "~/utils/pathBuilder"; - -export default function Page() { - return ( - - - - - - - Logout - - - - Create new Organizations and manage your account. - - - - - - - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/OrgAdminHeader.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/OrgAdminHeader.tsx deleted file mode 100644 index 073a0f276..000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/OrgAdminHeader.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { LinkButton } from "~/components/primitives/Buttons"; -import { - PageHeader, - PageTitleRow, - PageTitle, - PageButtons, - PageDescription, - PageTabs, -} from "~/components/primitives/PageHeader"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { - newProjectPath, - organizationBillingPath, - organizationPath, - organizationTeamPath, - organizationsPath, -} from "~/utils/pathBuilder"; - -export function OrgAdminHeader() { - const organization = useOrganization(); - - return ( - - - - - - Create a new project - - - - Manage your projects and team. - - - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx index a23eca60f..ce1fdf340 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx @@ -1,20 +1,44 @@ import { Link } from "@remix-run/react"; import simplur from "simplur"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { LinkButton } from "~/components/primitives/Buttons"; import { Header3 } from "~/components/primitives/Headers"; import { NamedIcon } from "~/components/primitives/NamedIcon"; +import { + PageHeader, + PageTitleRow, + PageTitle, + PageButtons, +} from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { useOrganization } from "~/hooks/useOrganizations"; +import { Handle } from "~/utils/handle"; import { newProjectPath, projectPath } from "~/utils/pathBuilder"; -import { OrgAdminHeader } from "./OrgAdminHeader"; + +export const handle: Handle = { + breadcrumb: (match) => , +}; export default function Page() { const organization = useOrganization(); return ( - + + + + + + Create a new project + + + +
      {organization.projects.length > 0 ? ( @@ -22,13 +46,13 @@ export default function Page() { return (
    • {project.name} - {simplur`${project._count.jobs} job[|s]`} + {simplur`${project.jobCount} Job[|s]`}
    • diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx index e0a38a6e6..763e99551 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx @@ -11,13 +11,26 @@ import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { TextLink } from "~/components/primitives/TextLink"; import { useOrganization } from "~/hooks/useOrganizations"; -import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder"; -import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader"; +import { + OrganizationParamsSchema, + jobPath, + newProjectPath, + organizationTeamPath, +} from "~/utils/pathBuilder"; import { Link } from "@remix-run/react/dist/components"; import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server"; import { requireUserId } from "~/services/session.server"; +import { LinkButton } from "~/components/primitives/Buttons"; +import { + PageHeader, + PageTitleRow, + PageTitle, + PageButtons, +} from "~/components/primitives/PageHeader"; +import { Handle } from "~/utils/handle"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request); @@ -47,13 +60,21 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps) return null; }; +export const handle: Handle = { + breadcrumb: (match) => , +}; + export default function Page() { const organization = useOrganization(); const loaderData = useTypedLoaderData(); return ( - + + + + +
      @@ -151,7 +172,7 @@ export default function Page() {
      -
      +
      Jobs Runs diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx similarity index 74% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx index 8a9a1c476..2778bfd1d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx @@ -1,15 +1,15 @@ import { ChevronRightIcon } from "@heroicons/react/24/solid"; import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { IntegrationIcon } from "~/assets/icons/IntegrationIcon"; import { Feedback } from "~/components/Feedback"; -import { LogoIcon } from "~/components/LogoIcon"; import { HowToConnectAnIntegration } from "~/components/helpContent/HelpContentText"; import { ConnectToIntegrationSheet } from "~/components/integrations/ConnectToIntegrationSheet"; import { IntegrationWithMissingFieldSheet } from "~/components/integrations/IntegrationWithMissingFieldSheet"; import { NoIntegrationSheet } from "~/components/integrations/NoIntegrationSheet"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { DateTime } from "~/components/primitives/DateTime"; @@ -17,7 +17,7 @@ import { DetailCell } from "~/components/primitives/DetailCell"; import { Header2 } from "~/components/primitives/Headers"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; import { Input } from "~/components/primitives/Input"; -import { NamedIcon, NamedIconInBox } from "~/components/primitives/NamedIcon"; +import { NamedIcon } from "~/components/primitives/NamedIcon"; import { PageButtons, PageDescription, @@ -25,7 +25,6 @@ import { PageTitle, PageTitleRow, } from "~/components/primitives/PageHeader"; -import { Paragraph } from "~/components/primitives/Paragraph"; import { Switch } from "~/components/primitives/Switch"; import { Table, @@ -38,10 +37,9 @@ import { TableRow, } from "~/components/primitives/Table"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; -import { useOrganization } from "~/hooks/useOrganizations"; +import { MatchedOrganization, useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useTextFilter } from "~/hooks/useTextFilter"; -import { Organization } from "~/models/organization.server"; import { Project } from "~/models/project.server"; import { Client, @@ -51,20 +49,21 @@ import { import { requireUser } from "~/services/session.server"; import { Handle } from "~/utils/handle"; import { + OrganizationParamsSchema, ProjectParamSchema, docsCreateIntegration, + docsPath, integrationClientPath, } from "~/utils/pathBuilder"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug } = OrganizationParamsSchema.parse(params); const presenter = new IntegrationsPresenter(); const data = await presenter.call({ userId: user.id, organizationSlug, - projectSlug: projectParam, }); return typedjson(data); @@ -72,14 +71,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { export const handle: Handle = { breadcrumb: (match) => , - expandSidebar: true, }; export default function Integrations() { const { clients, clientMissingFields, options, callbackUrl } = useTypedLoaderData(); const organization = useOrganization(); - const project = useProject(); return ( @@ -87,8 +84,12 @@ export default function Integrations() { - - Create your own Integration + + Integrations documentation @@ -113,11 +114,7 @@ export default function Integrations() { options={options} /> )} - +
      @@ -144,94 +141,96 @@ function PossibleIntegrationsList({ }); return ( -
      -
      - Connect an API - - Trigger.dev Integrations - - } +
      +
      +
      + Connect an API + + Trigger.dev Integrations + + } + /> +
      + setFilterText(e.target.value)} /> -
      - setFilterText(e.target.value)} - /> -
      - {filteredItems.map((option) => { - switch (option.type) { - case "integration": - return ( - - } - /> - ); - case "api": - return ( - - } - /> - ); +
      + {filteredItems.map((option) => { + switch (option.type) { + case "integration": + return ( + + } + /> + ); + case "api": + return ( + + } + /> + ); + } + })} +
      + Missing an API? + + + } - })} -
      - Missing an API? - - - - } - defaultValue="integration" - /> + defaultValue="integration" + /> - Create an Integration - - - + Create an Integration + + + +
      ); } @@ -239,11 +238,9 @@ function PossibleIntegrationsList({ function ConnectedIntegrationsList({ clients, organization, - project, }: { clients: Client[]; - organization: Organization; - project: Project; + organization: MatchedOrganization; }) { const { filterText, setFilterText, filteredItems } = useTextFilter({ items: clients, @@ -278,19 +275,20 @@ function ConnectedIntegrationsList({
      Your connected Integrations -
      {clients.length > 0 && (
      - setFilterText(e.target.value)} - /> +
      + setFilterText(e.target.value)} + /> + +
    @@ -318,7 +316,7 @@ function ConnectedIntegrationsList({ ) : ( <> {filteredItems.map((client) => { - const path = integrationClientPath(organization, project, client); + const path = integrationClientPath(organization, client); return ( {client.title} @@ -496,7 +494,3 @@ function AddIntegrationConnection({ /> ); } - -export function IntegrationIcon() { - return ; -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam._index/route.tsx similarity index 94% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam._index/route.tsx index 46bcdb351..f414b2434 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam._index/route.tsx @@ -3,7 +3,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { HowToUseThisIntegration } from "~/components/helpContent/HelpContentText"; import { JobSkeleton } from "~/components/jobs/JobSkeleton"; import { JobsTable } from "~/components/jobs/JobsTable"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Callout } from "~/components/primitives/Callout"; import { Header2 } from "~/components/primitives/Headers"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; @@ -23,14 +23,12 @@ import { export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam, clientParam } = - IntegrationClientParamSchema.parse(params); + const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); const jobsPresenter = new JobListPresenter(); const jobs = await jobsPresenter.call({ userId, - projectSlug: projectParam, organizationSlug, integrationSlug: clientParam, }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.connections/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.connections/route.tsx similarity index 93% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.connections/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.connections/route.tsx index b94e29e1d..393cf061c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.connections/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.connections/route.tsx @@ -1,7 +1,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { connectionType } from "~/components/integrations/connectionType"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { DateTime } from "~/components/primitives/DateTime"; import { Paragraph } from "~/components/primitives/Paragraph"; import { @@ -20,14 +20,12 @@ import { IntegrationClientParamSchema, trimTrailingSlash } from "~/utils/pathBui export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam, clientParam } = - IntegrationClientParamSchema.parse(params); + const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); const presenter = new IntegrationClientConnectionsPresenter(); const { connections } = await presenter.call({ userId: userId, organizationSlug, - projectSlug: projectParam, clientSlug: clientParam, }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.scopes/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.scopes/route.tsx similarity index 87% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.scopes/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.scopes/route.tsx index 32a3ed0f3..032e7a8ae 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam.scopes/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.scopes/route.tsx @@ -1,6 +1,6 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Paragraph } from "~/components/primitives/Paragraph"; import { IntegrationClientScopesPresenter } from "~/presenters/IntegrationClientScopesPresenter.server"; import { requireUserId } from "~/services/session.server"; @@ -9,14 +9,12 @@ import { IntegrationClientParamSchema, trimTrailingSlash } from "~/utils/pathBui export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam, clientParam } = - IntegrationClientParamSchema.parse(params); + const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); const presenter = new IntegrationClientScopesPresenter(); const { scopes } = await presenter.call({ userId: userId, organizationSlug, - projectSlug: projectParam, clientSlug: clientParam, }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route.tsx similarity index 86% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route.tsx index f78cb2818..cf3d2a457 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.integrations_.$clientParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route.tsx @@ -3,7 +3,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { connectionType } from "~/components/integrations/connectionType"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { ClipboardField } from "~/components/primitives/ClipboardField"; import { DateTime } from "~/components/primitives/DateTime"; import { @@ -26,19 +26,17 @@ import { integrationClientConnectionsPath, integrationClientPath, integrationClientScopesPath, - projectIntegrationsPath, + organizationIntegrationsPath, } from "~/utils/pathBuilder"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); - const { organizationSlug, clientParam, projectParam } = - IntegrationClientParamSchema.parse(params); + const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); const presenter = new IntegrationClientPresenter(); const client = await presenter.call({ userId: user.id, organizationSlug, - projectSlug: projectParam, clientSlug: clientParam, }); @@ -59,23 +57,22 @@ export const handle: Handle = { export default function Integrations() { const { client } = useTypedLoaderData(); const organization = useOrganization(); - const project = useProject(); let tabs = [ { label: "Jobs", - to: integrationClientPath(organization, project, client), + to: integrationClientPath(organization, client), }, ]; if (client.authMethod.type !== "local") { tabs.push({ label: "Connections", - to: integrationClientConnectionsPath(organization, project, client), + to: integrationClientConnectionsPath(organization, client), }); tabs.push({ label: "Scopes", - to: integrationClientScopesPath(organization, project, client), + to: integrationClientScopesPath(organization, client), }); } @@ -86,7 +83,7 @@ export default function Integrations() { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx index 8430ff60d..c08215b40 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx @@ -8,6 +8,7 @@ import simplur from "simplur"; import invariant from "tiny-invariant"; import { z } from "zod"; import { MainCenteredContainer } from "~/components/layout/AppLayout"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormButtons } from "~/components/primitives/FormButtons"; @@ -17,12 +18,16 @@ import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { env } from "~/env.server"; -import { useOrganization } from "~/hooks/useOrganizations"; +import { organizationMatchId, useOrganization } from "~/hooks/useOrganizations"; +import { useTypedMatchData } from "~/hooks/useTypedMatchData"; import { inviteMembers } from "~/models/member.server"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { scheduleEmail } from "~/services/email.server"; import { requireUserId } from "~/services/session.server"; +import { Handle } from "~/utils/handle"; import { acceptInvitePath, organizationTeamPath } from "~/utils/pathBuilder"; +import { loader } from "../_app.orgs.$organizationSlug/route"; +import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon"; const schema = z.object({ emails: z.preprocess((i) => { @@ -85,6 +90,20 @@ export const action: ActionFunction = async ({ request, params }) => { } }; +export const handle: Handle = { + breadcrumb: (match, matches) => { + const orgMatch = matches.find((m) => m.id === organizationMatchId); + const data = useTypedMatchData(orgMatch); + return ( + + {data && } + + + + ); + }, +}; + export default function Page() { const organization = useOrganization(); const lastSubmission = useActionData(); @@ -107,7 +126,7 @@ export default function Page() {
    diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx index d3503b8f2..c7b661d89 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx @@ -4,7 +4,6 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { FrameworkSelector } from "~/components/frameworks/FrameworkSelector"; import { JobsTable } from "~/components/jobs/JobsTable"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; import { Callout } from "~/components/primitives/Callout"; import { Header2 } from "~/components/primitives/Headers"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; @@ -27,20 +26,15 @@ import { useProject } from "~/hooks/useProject"; import { JobListPresenter } from "~/presenters/JobListPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import { Handle } from "~/utils/handle"; -import { - ProjectParamSchema, - projectIntegrationsPath, - trimTrailingSlash, -} from "~/utils/pathBuilder"; +import { ProjectParamSchema, organizationIntegrationsPath } from "~/utils/pathBuilder"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); try { const presenter = new JobListPresenter(); - const jobs = await presenter.call({ userId, projectSlug: projectParam }); + const jobs = await presenter.call({ userId, organizationSlug, projectSlug: projectParam }); return typedjson({ jobs, @@ -54,11 +48,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { } }; -export const handle: Handle = { - breadcrumb: (match) => , - expandSidebar: true, -}; - export default function Page() { const organization = useOrganization(); const project = useProject(); @@ -78,8 +67,8 @@ export default function Page() { - - + + @@ -94,7 +83,7 @@ export default function Page() { {jobs.some((j) => j.hasIntegrationsRequiringAction) && ( Some of your Jobs have Integrations that have not been configured. @@ -200,7 +189,7 @@ function ExampleJobs() { {example.icon} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/FirstEndpointSheet.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/FirstEndpointSheet.tsx index 5e3831689..6c59413d4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/FirstEndpointSheet.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/FirstEndpointSheet.tsx @@ -29,7 +29,6 @@ import { SelectValue, } from "~/components/primitives/Select"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { useRef, useState } from "react"; type FirstEndpointSheetProps = { projectId: string; @@ -52,16 +51,16 @@ export function FirstEndpointSheet({ projectId, environments }: FirstEndpointShe return ( - Add your first endpoint +
    Add your first endpoint - We recommend you use{" "} - the CLI when working - in development. + We recommend you{" "} + use the CLI when + working in development.
    diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx index 8be34a8d7..77f41351c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environments/route.tsx @@ -10,14 +10,15 @@ import { import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel"; import { HowToUseApiKeysAndEndpoints } from "~/components/helpContent/HelpContentText"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Badge } from "~/components/primitives/Badge"; -import { ButtonContent } from "~/components/primitives/Buttons"; +import { ButtonContent, LinkButton } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; import { DateTime } from "~/components/primitives/DateTime"; import { Header2, Header3 } from "~/components/primitives/Headers"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; import { + PageButtons, PageDescription, PageHeader, PageTitle, @@ -39,7 +40,11 @@ import { ClientEndpoint, EnvironmentsPresenter } from "~/presenters/Environments import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { Handle } from "~/utils/handle"; -import { ProjectParamSchema, projectEnvironmentsStreamingPath } from "~/utils/pathBuilder"; +import { + ProjectParamSchema, + docsPath, + projectEnvironmentsStreamingPath, +} from "~/utils/pathBuilder"; import { requestUrl } from "~/utils/requestUrl.server"; import { RuntimeEnvironmentType } from "../../../../../packages/database/src"; import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet"; @@ -74,8 +79,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; export const handle: Handle = { - breadcrumb: (match) => , - expandSidebar: true, + breadcrumb: (match) => , }; export default function Page() { @@ -128,6 +132,15 @@ export default function Page() { + + + Environments documentation + + API Keys and endpoints for your environments. @@ -164,13 +177,13 @@ export default function Page() { className="w-full max-w-none" secure value={environment.apiKey} - variant={"primary/medium"} + variant={"secondary/medium"} icon={Server} /> Public} /> @@ -234,12 +247,9 @@ export default function Page() { )) ) : ( - <> - Add your first endpoint - - - - + + + )} {selectedEndpoint && selectedEndpoint.endpoint && ( @@ -285,7 +295,7 @@ function EndpointRow({ The {environmentTitle({ type })} environment is not configured - Configure + Configure diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints.$httpEndpointParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints.$httpEndpointParam/route.tsx new file mode 100644 index 000000000..f1af7770f --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints.$httpEndpointParam/route.tsx @@ -0,0 +1,236 @@ +import { CheckIcon } from "@heroicons/react/20/solid"; +import { StopIcon } from "@heroicons/react/24/outline"; +import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { HowToConnectHttpEndpoint } from "~/components/helpContent/HelpContentText"; +import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; +import { LinkButton } from "~/components/primitives/Buttons"; +import { ClipboardField } from "~/components/primitives/ClipboardField"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Header1 } from "~/components/primitives/Headers"; +import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; +import { + PageButtons, + PageHeader, + PageTitle, + PageTitleRow, +} from "~/components/primitives/PageHeader"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { TextLink } from "~/components/primitives/TextLink"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { useTypedMatchData } from "~/hooks/useTypedMatchData"; +import { HttpEndpointPresenter } from "~/presenters/HttpEndpointPresenter.server"; +import { requireUserId } from "~/services/session.server"; +import { cn } from "~/utils/cn"; +import { Handle } from "~/utils/handle"; +import { HttpEndpointParamSchema, docsPath, projectHttpEndpointsPath } from "~/utils/pathBuilder"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const userId = await requireUserId(request); + const { projectParam, httpEndpointParam } = HttpEndpointParamSchema.parse(params); + + const presenter = new HttpEndpointPresenter(); + try { + const result = await presenter.call({ + userId, + projectSlug: projectParam, + httpEndpointKey: httpEndpointParam, + }); + + if (!result) { + throw new Response("Not Found", { status: 404 }); + } + + return typedjson(result); + } catch (e) { + console.log(e); + throw new Response(e instanceof Error ? e.message : JSON.stringify(e), { status: 404 }); + } +}; + +export const handle: Handle = { + breadcrumb: (match) => { + const data = useTypedMatchData(match); + return ( + + ); + }, +}; + +export default function Page() { + const { httpEndpoint, unconfiguredEnvironments, secret } = useTypedLoaderData(); + const organization = useOrganization(); + const project = useProject(); + + return ( + + + + + + + HTTP endpoints documentation + + + + + + + {(open) => ( +
    +
    +
    + + {httpEndpoint.httpEndpointEnvironments.length > 0 + ? "Ready to receive data" + : "Not deployed"} + + +
    + {httpEndpoint.httpEndpointEnvironments.length > 0 && ( +
    +
    + + + Environment + Endpoint URL + Secret + Source + Respond to request? + Updated + + + + {httpEndpoint.httpEndpointEnvironments.map((httpEnvironment) => { + return ( + + + + + + + + + + + {httpEnvironment.source} + + {httpEnvironment.immediateResponseFilter ? ( + + ) : ( + + )} + + + + + + ); + })} + +
    +
    + )} + + {unconfiguredEnvironments.length > 0 && ( + <> + Not deployed + + You need to deploy your code for the following environments to receive + webhooks –{" "} + + read our deployment guide + + . + +
    + + + + Environment + Endpoint URL + Secret + + + + {unconfiguredEnvironments.map((environment) => { + return ( + + + + + + + + + + + + ); + })} + +
    +
    + + )} +
    + + + +
    + )} + + + + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints._index/route.tsx new file mode 100644 index 000000000..52ed9ca8c --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints._index/route.tsx @@ -0,0 +1,136 @@ +import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { WhatAreHttpEndpoints } from "~/components/helpContent/HelpContentText"; +import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { LinkButton } from "~/components/primitives/Buttons"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; +import { Icon } from "~/components/primitives/Icon"; +import { + PageButtons, + PageDescription, + PageHeader, + PageTitle, + PageTitleRow, +} from "~/components/primitives/PageHeader"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { + Table, + TableBlankRow, + TableBody, + TableCell, + TableCellChevron, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { HttpEndpointsPresenter } from "~/presenters/HttpEndpointsPresenter.server"; +import { requireUserId } from "~/services/session.server"; +import { cn } from "~/utils/cn"; +import { ProjectParamSchema, docsPath, projectHttpEndpointPath } from "~/utils/pathBuilder"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const userId = await requireUserId(request); + const { projectParam } = ProjectParamSchema.parse(params); + + const presenter = new HttpEndpointsPresenter(); + const httpEndpoints = await presenter.call({ userId, slug: projectParam }); + + return typedjson({ httpEndpoints }); +}; + +export default function Page() { + const { httpEndpoints } = useTypedLoaderData(); + const organization = useOrganization(); + const project = useProject(); + + return ( + + + + + + + HTTP endpoints documentation + + + + + HTTP endpoints allow you to receive webhooks from any API. + + + + + {(open) => ( +
    +
    +
    + +
    +
    + + + + ID + Title + Updated + Environments + Go to page + + + + {httpEndpoints.length > 0 ? ( + httpEndpoints.map((httpEndpoint) => { + const path = projectHttpEndpointPath(organization, project, httpEndpoint); + return ( + + +
    + + {httpEndpoint.key} +
    +
    + {httpEndpoint.title ?? "–"} + + + + +
    + {httpEndpoint.httpEndpointEnvironments.map((environment) => ( + + ))} +
    +
    + +
    + ); + }) + ) : ( + + No HTTP endpoints + + )} +
    +
    +
    +
    + + + +
    + )} +
    +
    +
    + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints/route.tsx new file mode 100644 index 000000000..bbaf2d369 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.http-endpoints/route.tsx @@ -0,0 +1,11 @@ +import { Outlet } from "@remix-run/react"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; +import { Handle } from "~/utils/handle"; + +export const handle: Handle = { + breadcrumb: (match) => , +}; + +export default function Page() { + return ; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route.tsx index 17dcd7ab4..ceeb43196 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route.tsx @@ -1,27 +1,24 @@ import { useNavigation } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import simplur from "simplur"; import { z } from "zod"; import { HowToRunYourJob } from "~/components/helpContent/HelpContentText"; +import { Callout } from "~/components/primitives/Callout"; import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; import { RunsTable } from "~/components/runs/RunsTable"; +import { useJob } from "~/hooks/useJob"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; import { RunListPresenter } from "~/presenters/RunListPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import { Handle } from "~/utils/handle"; import { JobParamsSchema, - projectIntegrationsPath, jobRunsParentPath, - trimTrailingSlash, + organizationIntegrationsPath, } from "~/utils/pathBuilder"; import { ListPagination } from "./ListPagination"; -import { Callout } from "~/components/primitives/Callout"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { useJob } from "~/hooks/useJob"; -import simplur from "simplur"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]); @@ -53,10 +50,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }); }; -export const handle: Handle = { - breadcrumb: (match) => , -}; - export default function Page() { const { list } = useTypedLoaderData(); const navigation = useNavigation(); @@ -68,11 +61,7 @@ export default function Page() { return ( <> {job.hasIntegrationsRequiringAction && ( - + {simplur`This Job has ${ job.integrations.filter((j) => j.setupStatus === "MISSING_FIELDS").length } Integration[|s] that [has|have] not been configured.`} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx index a2bdb1fd2..8cdbcec4f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx @@ -3,7 +3,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Fragment, useEffect } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { useEventSource } from "remix-utils/sse/react"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon"; import { RunOverview } from "~/components/run/RunOverview"; import { jobMatchId, useJob } from "~/hooks/useJob"; @@ -50,8 +50,6 @@ export const handle: Handle = { return ( - - {runData && runData.run && ( )} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.settings/route.tsx index c52c1e8d2..fdfdbec59 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.settings/route.tsx @@ -1,6 +1,7 @@ import { JobEnvironment, JobStatusTable } from "~/components/JobsStatusTable"; import { HowToDisableAJob } from "~/components/helpContent/HelpContentText"; import { DeleteJobDialogContent } from "~/components/jobs/DeleteJobModalContent"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Button } from "~/components/primitives/Buttons"; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog"; import { Header2 } from "~/components/primitives/Headers"; @@ -10,8 +11,13 @@ import { useJob } from "~/hooks/useJob"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { cn } from "~/utils/cn"; +import { Handle } from "~/utils/handle"; import { projectJobsPath, projectPath } from "~/utils/pathBuilder"; +export const handle: Handle = { + breadcrumb: (match) => , +}; + export default function Page() { const job = useJob(); const organization = useOrganization(); @@ -45,15 +51,14 @@ export default function Page() { - - - + Delete Job +
    diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx index 73640b5a1..e8654c32f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.test/route.tsx @@ -9,7 +9,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { JSONEditor } from "~/components/code/JSONEditor"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { DateTime } from "~/components/primitives/DateTime"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route.tsx index 72af5ee25..dffd6cd79 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route.tsx @@ -4,10 +4,10 @@ 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"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon"; -import { LinkButton } from "~/components/primitives/Buttons"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; import { NamedIcon } from "~/components/primitives/NamedIcon"; import { PageButtons, @@ -24,9 +24,12 @@ import { useJob } from "~/hooks/useJob"; import { useOrganization } from "~/hooks/useOrganizations"; import { projectMatchId, useProject } from "~/hooks/useProject"; import { useOptionalRun } from "~/hooks/useRun"; +import { useTypedMatchData } from "~/hooks/useTypedMatchData"; import { findJobByParams } from "~/models/job.server"; import { JobListPresenter } from "~/presenters/JobListPresenter.server"; +import { JobPresenter } from "~/presenters/JobPresenter.server"; import { requireUserId } from "~/services/session.server"; +import { titleCase } from "~/utils"; import { Handle } from "~/utils/handle"; import { JobParamsSchema, @@ -41,43 +44,34 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { jobParam, projectParam, organizationSlug } = JobParamsSchema.parse(params); - const jobsPresenter = new JobListPresenter(); + const presenter = new JobPresenter(); + const job = await presenter.call({ + userId, + jobSlug: jobParam, + organizationSlug, + projectSlug: projectParam, + }); - const [job, projectJobs] = await Promise.all([ - findJobByParams({ - userId, - slug: jobParam, - projectSlug: projectParam, - organizationSlug, - }), - jobsPresenter.call({ userId, projectSlug: projectParam }), - ]); - - if (job === null) { + if (!job) { throw new Response("Not Found", { status: 404, statusText: `There is no Job ${jobParam} in this Project.`, }); } - //todo identify job - // analytics.job.identify({ job }); - return typedjson({ job, - projectJobs, }); }; export const handle: Handle = { - breadcrumb: (_match, matches) => { - const projectMatch = matches.find((m) => m.id === projectMatchId); + breadcrumb: (match) => { + const data = useTypedMatchData(match); return ( - - - - - + ); }, }; @@ -111,7 +105,12 @@ export default function Job() { - + {job.dynamic && } {job.properties && @@ -138,14 +137,7 @@ export default function Job() { - } + value={titleCase(job.status.toLowerCase())} /> @@ -154,14 +146,17 @@ export default function Job() { + + {job.noRunsHelp && ( + + {job.noRunsHelp.text} + + )} + , @@ -42,71 +32,69 @@ export default function SetUpAstro() { const appOrigin = useAppOrigin(); return ( - -
    -
    - -
    -
    - - Get setup in 5 minutes - -
    - - Choose a different framework - - - I'm stuck! - - } - defaultValue="help" - /> -
    -
    -
    - +
    + +
    +
    + + Get setup in 5 minutes + +
    + - Trigger.dev has full support for serverless. We will be adding support for long-running - servers soon. - -
    - - - - - - You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple - example Job in example.ts to help you - get started. - - - - - - - - - - - - - This page will automatically refresh. - -
    + Choose a different framework +
    + + I'm stuck! + + } + defaultValue="help" + />
    - +
    + + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + +
    + + + + + + You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple + example Job in example.ts to help you + get started. + + + + + + + + + + + + + This page will automatically refresh. + +
    +
    +
    ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx index 7823cf914..42179fbb3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.express/route.tsx @@ -2,11 +2,9 @@ import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/sol import invariant from "tiny-invariant"; import { ExpressLogo } from "~/assets/logos/ExpressLogo"; import { Feedback } from "~/components/Feedback"; -import { PageGradient } from "~/components/PageGradient"; -import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; +import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; import { StepContentContainer } from "~/components/StepContentContainer"; -import { InlineCode } from "~/components/code/InlineCode"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; @@ -35,86 +33,84 @@ export default function Page() { const appOrigin = useAppOrigin(); return ( - -
    -
    - -
    -
    - - Get setup in 5 minutes - -
    - - Choose a different framework - - - I'm stuck! - - } - defaultValue="help" - /> -
    -
    -
    - +
    + +
    +
    + + Get setup in 5 minutes + +
    + - Trigger.dev has full support for serverless. We will be adding support for long-running - servers soon. - -
    - - - Copy your server API Key to your clipboard: -
    - Server} - /> -
    - Now follow this guide: - - Manual installation guide - -
    - - - - - You may be using the `start` script instead, in which case substitute `dev` in the - above commands. - - - - - - - - - This page will automatically refresh. - -
    + Choose a different framework +
    + + I'm stuck! + + } + defaultValue="help" + />
    - +
    + + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + +
    + + + Copy your server API Key to your clipboard: +
    + Server} + /> +
    + Now follow this guide: + + Manual installation guide + +
    + + + + + You may be using the `start` script instead, in which case substitute `dev` in the + above commands. + + + + + + + + + This page will automatically refresh. + +
    +
    +
    ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx index a9eaa7469..4346b1b99 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.fastify/route.tsx @@ -1,6 +1,6 @@ import { FastifyLogo } from "~/assets/logos/FastifyLogo"; import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Handle } from "~/utils/handle"; import { trimTrailingSlash } from "~/utils/pathBuilder"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx index d13b547a0..0f221f53c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nestjs/route.tsx @@ -1,11 +1,12 @@ import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; import invariant from "tiny-invariant"; +import { NestjsLogo } from "~/assets/logos/NestjsLogo"; import { Feedback } from "~/components/Feedback"; -import { PageGradient } from "~/components/PageGradient"; +import { TriggerDevStep } from "~/components/SetupCommands"; import { StepContentContainer } from "~/components/StepContentContainer"; import { InlineCode } from "~/components/code/InlineCode"; import { InstallPackages } from "~/components/code/InstallPackages"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Header1 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; @@ -18,7 +19,6 @@ import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { Handle } from "~/utils/handle"; import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; import { CodeBlock } from "../../components/code/CodeBlock"; -import { TriggerDevStep } from "~/components/SetupCommands"; export const handle: Handle = { breadcrumb: (match) => , @@ -126,95 +126,96 @@ export default function SetupNestJS() { invariant(devEnvironment, "devEnvironment is required"); return ( - -
    -
    - - Get setup in 2 minutes - -
    - - Choose a different framework - - - I'm stuck! - - } - defaultValue="help" - /> -
    -
    - <> - - - - - - - - Inside your .env file, create the following env variables: - - - - - - - Now, go to your app.module.ts and add the{" "} - TriggerDevModule: - - - - - - - Create a controller called{" "} - job.controller.ts and add the following code: - - - - - - - Now, add the new controller to your{" "} - app.module.ts: - - - - - - - Now, add this to the top-level of your package.json: - - - - - - - Finally, run your project with npm run start: - - - - - - - - - This page will automatically refresh. - - +
    +
    +
    - +
    + + Get setup in 5 minutes + +
    + + Choose a different framework + + + I'm stuck! + + } + defaultValue="help" + /> +
    +
    + <> + + + + + + + + Inside your .env file, create the following env variables: + + + + + + + Now, go to your app.module.ts and add the{" "} + TriggerDevModule: + + + + + + + Create a controller called{" "} + job.controller.ts and add the following code: + + + + + + + Now, add the new controller to your{" "} + app.module.ts: + + + + + + + Now, add this to the top-level of your package.json: + + + + + + + Finally, run your project with npm run start: + + + + + + + + + This page will automatically refresh. + + +
    ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx index 9cb4f3175..09fb9bab6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nextjs/route.tsx @@ -1,13 +1,14 @@ import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; import { useState } from "react"; import invariant from "tiny-invariant"; +import { NextjsLogo } from "~/assets/logos/NextjsLogo"; import { Feedback } from "~/components/Feedback"; -import { PageGradient } from "~/components/PageGradient"; import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; import { StepContentContainer } from "~/components/StepContentContainer"; import { InlineCode } from "~/components/code/InlineCode"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; import { ClientTabs, ClientTabsContent, @@ -21,14 +22,12 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton"; import { StepNumber } from "~/components/primitives/StepNumber"; import { useAppOrigin } from "~/hooks/useAppOrigin"; -import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { useDevEnvironment } from "~/hooks/useEnvironments"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; +import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { Handle } from "~/utils/handle"; import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; -import { Callout } from "~/components/primitives/Callout"; -import { NextjsLogo } from "~/assets/logos/NextjsLogo"; type SelectionChoices = "use-existing-project" | "create-new-next-app"; @@ -47,171 +46,169 @@ export default function SetupNextjs() { invariant(devEnvironment, "devEnvironment is required"); return ( - -
    -
    - -
    -
    - - Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes - -
    - - Choose a different framework - - - I'm stuck! - - } - defaultValue="help" - /> -
    -
    - setSelectedValue(value as SelectionChoices)} - > - } - /> - } - /> - - {selectedValue && ( - <> - - Trigger.dev has full support for serverless. We will be adding support for - long-running servers soon. - - {selectedValue === "create-new-next-app" ? ( - <> - - - - - npm - pnpm - yarn - - - - - - - - - - - - - - Trigger.dev works with either the Pages or App Router configuration. - - - - - - You have now created a new Next.js project. Let’s cd{" "} - into it using the project name you just provided: - - - - - - - - You’ll notice a new folder in your project called 'jobs'. We’ve added a very - simple example Job in examples.ts{" "} - to help you get started. - - - - - - - - - - - - - This page will automatically refresh. - - - ) : ( - <> - - - - - - You’ll notice a new folder in your project called 'jobs'. We’ve added a very - simple example Job in examples.ts{" "} - to help you get started. - - - - - - - - - - - - - This page will automatically refresh. - - - )} - - )} +
    +
    +
    - +
    + + Get setup in {selectedValue === "create-new-next-app" ? "5" : "2"} minutes + +
    + + Choose a different framework + + + I'm stuck! + + } + defaultValue="help" + /> +
    +
    + setSelectedValue(value as SelectionChoices)} + > + } + /> + } + /> + + {selectedValue && ( + <> + + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + + {selectedValue === "create-new-next-app" ? ( + <> + + + + + npm + pnpm + yarn + + + + + + + + + + + + + + Trigger.dev works with either the Pages or App Router configuration. + + + + + + You have now created a new Next.js project. Let’s cd into + it using the project name you just provided: + + + + + + + + You’ll notice a new folder in your project called 'jobs'. We’ve added a very + simple example Job in examples.ts{" "} + to help you get started. + + + + + + + + + + + + + This page will automatically refresh. + + + ) : ( + <> + + + + + + You’ll notice a new folder in your project called 'jobs'. We’ve added a very + simple example Job in examples.ts{" "} + to help you get started. + + + + + + + + + + + + + This page will automatically refresh. + + + )} + + )} +
    ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nuxt/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nuxt/route.tsx index 7ab0c20a9..f2560a2e7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nuxt/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.nuxt/route.tsx @@ -1,6 +1,6 @@ import { NuxtLogo } from "~/assets/logos/NuxtLogo"; import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Handle } from "~/utils/handle"; import { trimTrailingSlash } from "~/utils/pathBuilder"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.redwood/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.redwood/route.tsx index 8fea7be77..a6d5286fd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.redwood/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.redwood/route.tsx @@ -1,6 +1,6 @@ import { RedwoodLogo } from "~/assets/logos/RedwoodLogo"; import { FrameworkComingSoon } from "~/components/frameworks/FrameworkComingSoon"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Handle } from "~/utils/handle"; import { trimTrailingSlash } from "~/utils/pathBuilder"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx index 12a9397ca..18a8e3c27 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.remix/route.tsx @@ -1,33 +1,23 @@ import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; import invariant from "tiny-invariant"; +import { RemixLogo } from "~/assets/logos/RemixLogo"; import { Feedback } from "~/components/Feedback"; -import { PageGradient } from "~/components/PageGradient"; +import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; import { StepContentContainer } from "~/components/StepContentContainer"; import { InlineCode } from "~/components/code/InlineCode"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Button, LinkButton } from "~/components/primitives/Buttons"; -import { - ClientTabs, - ClientTabsContent, - ClientTabsList, - ClientTabsTrigger, -} from "~/components/primitives/ClientTabs"; -import { ClipboardField } from "~/components/primitives/ClipboardField"; +import { Callout } from "~/components/primitives/Callout"; import { Header1 } from "~/components/primitives/Headers"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; import { Paragraph } from "~/components/primitives/Paragraph"; import { StepNumber } from "~/components/primitives/StepNumber"; import { useAppOrigin } from "~/hooks/useAppOrigin"; -import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { useDevEnvironment } from "~/hooks/useEnvironments"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; +import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { Handle } from "~/utils/handle"; import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; -import { Callout } from "~/components/primitives/Callout"; -import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; -import { Badge } from "~/components/primitives/Badge"; -import { RemixLogo } from "~/assets/logos/RemixLogo"; export const handle: Handle = { breadcrumb: (match) => , @@ -42,71 +32,69 @@ export default function SetUpRemix() { const appOrigin = useAppOrigin(); return ( - -
    -
    - -
    -
    - - Get setup in 5 minutes - -
    - - Choose a different framework - - - I'm stuck! - - } - defaultValue="help" - /> -
    -
    -
    - +
    + +
    +
    + + Get setup in 5 minutes + +
    + - Trigger.dev has full support for serverless. We will be adding support for long-running - servers soon. - -
    - - - - - - You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple - example Job in example.server.ts to - help you get started. - - - - - - - - - - - - - This page will automatically refresh. - -
    + Choose a different framework +
    + + I'm stuck! + + } + defaultValue="help" + />
    - +
    + + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + +
    + + + + + + You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple + example Job in example.server.ts to + help you get started. + + + + + + + + + + + + + This page will automatically refresh. + +
    +
    +
    ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx index eb47e5fdf..3cdd1ab2b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup.sveltekit/route.tsx @@ -1,26 +1,23 @@ import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; import invariant from "tiny-invariant"; +import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo"; import { Feedback } from "~/components/Feedback"; -import { PageGradient } from "~/components/PageGradient"; import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands"; import { StepContentContainer } from "~/components/StepContentContainer"; -import { InlineCode } from "~/components/code/InlineCode"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; +import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; import { ClipboardField } from "~/components/primitives/ClipboardField"; import { Header1 } from "~/components/primitives/Headers"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; import { Paragraph } from "~/components/primitives/Paragraph"; import { StepNumber } from "~/components/primitives/StepNumber"; -import { useAppOrigin } from "~/hooks/useAppOrigin"; -import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { useDevEnvironment } from "~/hooks/useEnvironments"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; +import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete"; import { Handle } from "~/utils/handle"; import { projectSetupPath, trimTrailingSlash } from "~/utils/pathBuilder"; -import { Callout } from "~/components/primitives/Callout"; -import { Badge } from "~/components/primitives/Badge"; export const handle: Handle = { breadcrumb: (match) => ( @@ -34,80 +31,81 @@ export default function SetUpSveltekit() { const devEnvironment = useDevEnvironment(); invariant(devEnvironment, "Dev environment must be defined"); return ( - -
    -
    - - Get setup in 5 minutes - -
    - - Choose a different framework - - - I'm stuck! - - } - defaultValue="help" - /> -
    -
    -
    - +
    + +
    +
    + + Get setup in 5 minutes + +
    + - Trigger.dev has full support for serverless. We will be adding support for long-running - servers soon. - -
    - - - Copy your server API Key to your clipboard: -
    - Server} - /> -
    - Now follow this guide: - - Manual installation guide - -
    -
    - - - - - - - - - - - This page will automatically refresh. - -
    + Choose a different framework +
    + + I'm stuck! + + } + defaultValue="help" + />
    - +
    + + Trigger.dev has full support for serverless. We will be adding support for long-running + servers soon. + +
    + + + Copy your server API Key to your clipboard: +
    + Server} + /> +
    + Now follow this guide: + + Manual installation guide + +
    +
    + + + + + + + + + + + This page will automatically refresh. + +
    +
    +
    ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup/route.tsx index 5bee43084..ba8dbc1c3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.setup/route.tsx @@ -1,11 +1,11 @@ import { Outlet } from "@remix-run/react"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { Handle } from "~/utils/handle"; import { trimTrailingSlash } from "~/utils/pathBuilder"; export const handle: Handle = { breadcrumb: (match) => ( - + ), }; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers._index/route.tsx index c1c27be1e..fa5363c31 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers._index/route.tsx @@ -2,7 +2,6 @@ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; import { LabelValueStack } from "~/components/primitives/LabelValueStack"; import { NamedIcon } from "~/components/primitives/NamedIcon"; import { Paragraph } from "~/components/primitives/Paragraph"; @@ -22,8 +21,7 @@ import { useProject } from "~/hooks/useProject"; import { TriggersPresenter } from "~/presenters/TriggersPresenter.server"; import { requireUser } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import { Handle } from "~/utils/handle"; -import { ProjectParamSchema, externalTriggerPath, trimTrailingSlash } from "~/utils/pathBuilder"; +import { ProjectParamSchema, externalTriggerPath } from "~/utils/pathBuilder"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); @@ -39,13 +37,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { return typedjson(data); }; -export const handle: Handle = { - breadcrumb: (match) => ( - - ), - expandSidebar: true, -}; - export default function Integrations() { const { triggers } = useTypedLoaderData(); const organization = useOrganization(); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx index 02cfaa55d..e1468fdd9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers.scheduled/route.tsx @@ -3,7 +3,7 @@ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { DateTime } from "~/components/primitives/DateTime"; import { LabelValueStack } from "~/components/primitives/LabelValueStack"; import { NamedIcon } from "~/components/primitives/NamedIcon"; @@ -43,7 +43,6 @@ export const handle: Handle = { breadcrumb: (match) => ( ), - expandSidebar: true, }; export default function Integrations() { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers/route.tsx index c340d77c6..7830ff1e4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers/route.tsx @@ -1,6 +1,6 @@ import { Outlet } from "@remix-run/react"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { LinkButton } from "~/components/primitives/Buttons"; import { PageButtons, @@ -39,7 +39,7 @@ export default function Page() { to={docsPath("documentation/concepts/triggers")} variant="secondary/small" > - Triggers Documentation + Triggers documentation diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam/route.tsx index fcfef7cb7..0be482778 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam/route.tsx @@ -4,7 +4,7 @@ import { Fragment } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon"; import { Callout, variantClasses } from "~/components/primitives/Callout"; import { Header2 } from "~/components/primitives/Headers"; @@ -27,7 +27,7 @@ import { requireUser, requireUserId } from "~/services/session.server"; import { Handle } from "~/utils/handle"; import { TriggerSourceParamSchema, - organizationsPath, + rootPath, projectTriggersPath, externalTriggerPath, externalTriggerRunsParentPath, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam_.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam_.runs.$runParam/route.tsx index 0e14db661..becefab9f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam_.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.triggers_.external.$triggerParam_.runs.$runParam/route.tsx @@ -3,7 +3,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Fragment, useEffect } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { useEventSource } from "remix-utils/sse/react"; -import { BreadcrumbLink } from "~/components/navigation/NavBar"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon"; import { RunOverview } from "~/components/run/RunOverview"; import { prisma } from "~/db.server"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx index 45802ba25..c5e4fa41b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx @@ -3,13 +3,14 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson } from "remix-typedjson"; import invariant from "tiny-invariant"; import { RouteErrorDisplay } from "~/components/ErrorDisplay"; -import { ProjectSideMenu, SideMenuContainer } from "~/components/navigation/ProjectSideMenu"; -import { ProjectsMenu } from "~/components/navigation/ProjectsMenu"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; +import { useTypedMatchData } from "~/hooks/useTypedMatchData"; import { ProjectPresenter } from "~/presenters/ProjectPresenter.server"; -import { telemetry } from "~/services/telemetry.server"; +import { commitCurrentProjectSession, setCurrentProjectId } from "~/services/currentProject.server"; import { requireUserId } from "~/services/session.server"; +import { telemetry } from "~/services/telemetry.server"; import { Handle } from "~/utils/handle"; import { projectPath } from "~/utils/pathBuilder"; @@ -35,9 +36,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { telemetry.project.identify({ project }); - return typedjson({ - project, - }); + const session = await setCurrentProjectId(project.id, request); + + return typedjson( + { + project, + }, + { + headers: { "Set-Cookie": await commitCurrentProjectSession(session) }, + } + ); } catch (error) { if (error instanceof Response) { throw error; @@ -52,7 +60,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; export const handle: Handle = { - breadcrumb: (_match, matches) => , + breadcrumb: (match) => { + const data = useTypedMatchData(match); + return ; + }, scripts: (match) => [ { src: "https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js", @@ -64,12 +75,7 @@ export const handle: Handle = { export default function Project() { return ( <> - - -
    - -
    -
    + ); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.team/route.tsx index 56d23cb92..0fd9c10a3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.team/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.team/route.tsx @@ -30,8 +30,10 @@ import { getTeamMembersAndInvites, removeTeamMember } from "~/models/member.serv import { redirectWithSuccessMessage } from "~/models/message.server"; import { requireUserId } from "~/services/session.server"; import { inviteTeamMemberPath, organizationTeamPath, resendInvitePath } from "~/utils/pathBuilder"; -import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader"; import { DateTime } from "~/components/primitives/DateTime"; +import { PageHeader, PageTitleRow, PageTitle } from "~/components/primitives/PageHeader"; +import { BreadcrumbLink } from "~/components/navigation/Breadcrumb"; +import { Handle } from "~/utils/handle"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -93,6 +95,10 @@ export const action: ActionFunction = async ({ request, params }) => { type Member = UseDataFunctionReturn["members"][number]; type Invite = UseDataFunctionReturn["invites"][number]; +export const handle: Handle = { + breadcrumb: (match) => , +}; + export default function Page() { const user = useUser(); const { members, invites } = useTypedLoaderData(); @@ -100,10 +106,14 @@ export default function Page() { return ( - + + + + + Members -