Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f275b47755 | |||
| cfb7481d7e | |||
| 57fcce90d9 | |||
| 2c0ea0c108 | |||
| e81317b72c | |||
| 946e429906 | |||
| 2e5a401ff7 | |||
| b8b84d8d16 | |||
| 039777bf63 | |||
| 3547ec13c3 | |||
| 3763577451 | |||
| c6618049f2 | |||
| 28d6aa9885 | |||
| 568786e4f6 | |||
| 38614ee526 | |||
| 15ad6bb5f8 | |||
| 15f431ada6 | |||
| 5163acc6ef | |||
| f9297d783a | |||
| c34a02c06f | |||
| 61ed1fb202 | |||
| 87638ba6c5 | |||
| 26b4100bb0 | |||
| 1157c2adf6 | |||
| 8613d3288b | |||
| ab008fbdff | |||
| 0b30197280 | |||
| 89a121f21e | |||
| 045a8c56af | |||
| d83c11db8c | |||
| 7efe14b835 | |||
| 37116aecf9 | |||
| 9a606e8f2a | |||
| b1e65ed500 | |||
| 26f3dbff06 | |||
| 5ee0b188bd | |||
| 4692cd6a55 | |||
| 7ad8c01853 | |||
| bc27b45fd7 | |||
| 94cc28d377 | |||
| 19e80cef3b | |||
| 5397cd3194 | |||
| f01af9c0ec | |||
| a288010d02 | |||
| b9a7a1c32b | |||
| 65b9a0dce5 | |||
| 7464006e19 | |||
| 590d56b7e7 | |||
| c75cb1e87e | |||
| 597b2f4a6f | |||
| 2e84c6bbb2 | |||
| 06c0cd5297 | |||
| 79eab3d62f | |||
| 6527723a0e | |||
| 3d6652f132 | |||
| da5d9779d4 | |||
| 865399e753 | |||
| 960ea6c8dd | |||
| 39ab48f0cb | |||
| 7341b4cf61 | |||
| 28c5acc6af | |||
| 4366eefc66 | |||
| cb95e761c1 | |||
| 7136748463 | |||
| b0cacfd75c | |||
| 81f03dac79 | |||
| e37d1eadcd | |||
| 23fb310374 | |||
| 797a1732d2 | |||
| c405a417e9 | |||
| 808deb1d77 | |||
| 6ac1473ec3 | |||
| 9c183abf4b | |||
| 33c33faae9 | |||
| 4cccbc2039 | |||
| cc061dedf8 | |||
| 402ce2210f | |||
| 329c5c4070 | |||
| 878aeb7a8c | |||
| 3ee396d788 | |||
| 090037602c | |||
| bee77051e7 | |||
| be6741a4a8 | |||
| 3e9b440322 | |||
| dfea5bc77c | |||
| 1eef30e298 | |||
| e5b9a9de3f | |||
| dfedd3a6e8 | |||
| a19845b4ca | |||
| 094f6f5a7a | |||
| fa60edce2b | |||
| ba44652457 | |||
| 931be39912 | |||
| 5048a27acb | |||
| 71fc615b31 | |||
| 20ed4e22ef | |||
| fc2d337a39 | |||
| 31d96f3862 | |||
| 7b43853ec6 | |||
| 40fa913272 | |||
| cdd5482aad | |||
| 4a3fa20ee7 | |||
| d49ed4cd47 | |||
| db3cf77f5b | |||
| c4a08961d6 | |||
| 5ba1643d46 | |||
| d189f425e8 | |||
| 0fe6fb83ac | |||
| f15ae41f48 | |||
| 46a07fc72e | |||
| 83af317739 | |||
| f4db1b2735 | |||
| 6d4922f419 | |||
| 6af098ee8a |
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/openai": patch
|
||||
---
|
||||
|
||||
Improved OpenAI task errors
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Don't return the apiKey when they don't match
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
getAllResponses fixed
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/plain": patch
|
||||
---
|
||||
|
||||
Added upsertCustomTimelineEntry Task and exported the components from the plain package
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/slack": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Upgrade to zod 3.21.4
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
api.trigger.dev is now the default cloud url
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
cronTrigger jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
intervalTrigger() jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
jsdocs for DynamicTrigger and DynamicSchedule
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
"@trigger.dev/integration-kit": patch
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/github": patch
|
||||
"@trigger.dev/openai": patch
|
||||
"@trigger.dev/resend": patch
|
||||
"@trigger.dev/plain": patch
|
||||
"@trigger.dev/slack": patch
|
||||
"@trigger.dev/nextjs": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Set Node version to 16.8 and above
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Loosen the Next.js dependency version
|
||||
+22
-1
@@ -15,7 +15,9 @@
|
||||
"@trigger.dev/init": "0.2.0",
|
||||
"@trigger.dev/integration-kit": "2.0.0-next.1",
|
||||
"@trigger.dev/resend": "1.0.0-next.1",
|
||||
"@trigger.dev/cli": "0.2.1-next.8"
|
||||
"@trigger.dev/cli": "0.2.1-next.8",
|
||||
"@trigger.dev/typeform": "1.0.0-next.2",
|
||||
"@trigger.dev/plain": "1.0.0-next.2"
|
||||
},
|
||||
"changesets": [
|
||||
"afraid-hornets-cheer",
|
||||
@@ -23,16 +25,22 @@
|
||||
"big-tables-search",
|
||||
"bright-buses-join",
|
||||
"bright-horses-bathe",
|
||||
"brown-bikes-bathe",
|
||||
"chatty-cars-agree",
|
||||
"chilly-meals-warn",
|
||||
"clever-bats-learn",
|
||||
"cuddly-pets-arrive",
|
||||
"curly-hornets-sort",
|
||||
"dirty-needles-kneel",
|
||||
"dull-bulldogs-design",
|
||||
"eight-buckets-agree",
|
||||
"eighty-queens-prove",
|
||||
"fast-ligers-wink",
|
||||
"four-ligers-perform",
|
||||
"friendly-parents-clean",
|
||||
"funny-elephants-sing",
|
||||
"gentle-dogs-end",
|
||||
"gentle-guests-leave",
|
||||
"great-meals-listen",
|
||||
"healthy-rockets-cry",
|
||||
"heavy-eyes-own",
|
||||
@@ -41,33 +49,46 @@
|
||||
"hot-seals-press",
|
||||
"itchy-cars-count",
|
||||
"itchy-rockets-provide",
|
||||
"khaki-needles-wait",
|
||||
"long-clocks-hide",
|
||||
"lovely-hounds-accept",
|
||||
"lovely-plants-brake",
|
||||
"lucky-crews-matter",
|
||||
"metal-donkeys-relate",
|
||||
"modern-emus-suffer",
|
||||
"modern-turtles-eat",
|
||||
"moody-islands-melt",
|
||||
"odd-colts-smash",
|
||||
"pink-islands-pay",
|
||||
"pink-numbers-jam",
|
||||
"plenty-ties-raise",
|
||||
"popular-cooks-boil",
|
||||
"pretty-bats-obey",
|
||||
"quick-stingrays-own",
|
||||
"quiet-berries-tickle",
|
||||
"quiet-dryers-collect",
|
||||
"rich-clocks-attack",
|
||||
"rude-bees-taste",
|
||||
"shiny-mirrors-beam",
|
||||
"shy-bobcats-hear",
|
||||
"silent-mirrors-lay",
|
||||
"silver-insects-sneeze",
|
||||
"six-humans-happen",
|
||||
"sixty-pandas-deliver",
|
||||
"sixty-squids-wink",
|
||||
"strange-jobs-shout",
|
||||
"strong-cars-visit",
|
||||
"stupid-files-cough",
|
||||
"sweet-adults-applaud",
|
||||
"sweet-cups-rescue",
|
||||
"sweet-snails-refuse",
|
||||
"tame-flowers-invite",
|
||||
"tasty-bats-flow",
|
||||
"ten-birds-confess",
|
||||
"thirty-apes-happen",
|
||||
"tidy-planes-compete",
|
||||
"tough-walls-share",
|
||||
"two-tools-hide",
|
||||
"wild-rats-mix",
|
||||
"young-birds-unite"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
safeBodyParse webhook import fixed
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Initializing a next.js project now creates a better file structure
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Fix when setting the url as an option
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@trigger.dev/integration-kit": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/typeform": patch
|
||||
---
|
||||
|
||||
Creating the typeform integration package
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/openai": patch
|
||||
---
|
||||
|
||||
Adding support for output properties on tasks
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/typeform": patch
|
||||
"@trigger.dev/integration-kit": patch
|
||||
---
|
||||
|
||||
integration-kit uses webhook package
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Improved the endpoint prompt
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Improved spinner and made port clear in logs
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
### The background jobs framework for Next.js
|
||||
|
||||
[Discord](https://discord.gg/bpVDSjcG) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
|
||||
[Discord](https://discord.gg/JtBAxBr2m3) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
|
||||
|
||||
[](https://twitter.com/triggerdotdev)
|
||||
[](https://github.com/triggerdotdev/trigger.dev)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
|
||||
import { LinkButton } from "./primitives/Buttons";
|
||||
import { Header1, Header3 } from "./primitives/Headers";
|
||||
import { friendlyErrorDisplay } from "~/utils/httpErrors";
|
||||
|
||||
type ErrorDisplayOptions = {
|
||||
button?: {
|
||||
title: string;
|
||||
to: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function RouteErrorDisplay(options?: ErrorDisplayOptions) {
|
||||
const error = useRouteError();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRouteErrorResponse(error) ? (
|
||||
<ErrorDisplay
|
||||
title={friendlyErrorDisplay(error.status, error.statusText).title}
|
||||
message={
|
||||
error.data.message ??
|
||||
friendlyErrorDisplay(error.status, error.statusText).message
|
||||
}
|
||||
{...options}
|
||||
/>
|
||||
) : error instanceof Error ? (
|
||||
<ErrorDisplay title={error.name} message={error.message} {...options} />
|
||||
) : (
|
||||
<ErrorDisplay
|
||||
title="Oops"
|
||||
message={JSON.stringify(error)}
|
||||
{...options}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type DisplayOptionsProps = {
|
||||
title: string;
|
||||
message?: string;
|
||||
} & ErrorDisplayOptions;
|
||||
|
||||
export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
|
||||
{message && <Header3>{message}</Header3>}
|
||||
<LinkButton
|
||||
to={button ? button.to : "/"}
|
||||
variant="primary/medium"
|
||||
className="mt-8"
|
||||
>
|
||||
{button ? button.title : "Home"}
|
||||
</LinkButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { H, HighlightOptions } from "highlight.run";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface Props extends HighlightOptions {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export function HighlightInit({ projectId, ...highlightOptions }: Props) {
|
||||
useEffect(() => {
|
||||
projectId && H.init(projectId, highlightOptions);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -23,9 +23,9 @@ export function UserAvatar({
|
||||
className?: string;
|
||||
}) {
|
||||
return avatarUrl ? (
|
||||
<div>
|
||||
<div className={cn("grid aspect-square place-items-center", className)}>
|
||||
<img
|
||||
className={cn("aspect-square rounded-full p-[7%]", className)}
|
||||
className={cn("aspect-square rounded-full p-[7%]")}
|
||||
src={avatarUrl}
|
||||
alt={name ?? "User"}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Paragraph, TextLink } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
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 { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { jobTestPath } from "~/utils/pathBuilder";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import { HelpPanelProps } from "../integrations/ApiKeyHelp";
|
||||
import { HelpInstall } from "../integrations/HelpInstall";
|
||||
import { HelpSamples } from "../integrations/HelpSamples";
|
||||
import { Button, LinkButton } from "../primitives/Buttons";
|
||||
import { Callout, variantClasses } from "../primitives/Callout";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsContent,
|
||||
@@ -15,11 +24,9 @@ import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import integrationButton from "./integration-button.png";
|
||||
import selectEnvironment from "./select-environment.png";
|
||||
import selectExample from "./select-example.png";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import { ApiKeyHelp, HelpPanelProps } from "../integrations/ApiKeyHelp";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { HelpInstall } from "../integrations/HelpInstall";
|
||||
import { HelpSamples } from "../integrations/HelpSamples";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
|
||||
export function HowToSetupYourProject() {
|
||||
const devEnvironment = useDevEnvironment();
|
||||
@@ -140,6 +147,65 @@ export function HowToSetupYourProject() {
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToRunYourJob() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const job = useJob();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Callout variant="info" className="mb-6">
|
||||
<Paragraph variant={"small"} className={variantClasses.info.textColor}>
|
||||
Scheduled Triggers <strong>do not</strong> trigger Jobs in the DEV
|
||||
Environment. When developing locally you should use the{" "}
|
||||
<Link
|
||||
to={jobTestPath(organization, project, job)}
|
||||
className="underline underline-offset-2 transition hover:text-blue-100"
|
||||
>
|
||||
Test feature
|
||||
</Link>{" "}
|
||||
to trigger any scheduled Jobs.
|
||||
</Paragraph>
|
||||
</Callout>
|
||||
|
||||
<Paragraph spacing>There are two ways to run your Job:</Paragraph>
|
||||
|
||||
<StepNumber stepNumber="1" title="Trigger a test Run" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
You can perform a Run with any payload you want, or use one of our
|
||||
examples, on the test page.
|
||||
</Paragraph>
|
||||
<LinkButton
|
||||
to={jobTestPath(organization, project, job)}
|
||||
variant={"primary/small"}
|
||||
LeadingIcon={"beaker"}
|
||||
leadingIconClassName="text-bright"
|
||||
>
|
||||
Test
|
||||
</LinkButton>
|
||||
</StepContentContainer>
|
||||
|
||||
<StepNumber stepNumber="2" title="Trigger your Job for real" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Performing a real run depends on the type of Trigger your Job is
|
||||
using.
|
||||
</Paragraph>
|
||||
|
||||
<LinkButton
|
||||
to="https://trigger.dev/docs/documentation/guides/running-jobs"
|
||||
variant={"primary/small"}
|
||||
LeadingIcon={"docs"}
|
||||
leadingIconClassName="text-bright"
|
||||
>
|
||||
How to run a Job
|
||||
</LinkButton>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToRunATest() {
|
||||
return (
|
||||
<>
|
||||
@@ -171,7 +237,7 @@ export function HowToRunATest() {
|
||||
</StepContentContainer>
|
||||
<Callout
|
||||
variant="docs"
|
||||
href="https://trigger.dev/docs/documentation/guides/testing-jobs"
|
||||
to="https://trigger.dev/docs/documentation/guides/testing-jobs"
|
||||
>
|
||||
Learn more about running tests.
|
||||
</Callout>
|
||||
@@ -219,7 +285,7 @@ export function HowToConnectAnIntegration() {
|
||||
</StepContentContainer>
|
||||
<Callout
|
||||
variant={"docs"}
|
||||
href="https://trigger.dev/docs/integrations/introduction"
|
||||
to="https://trigger.dev/docs/integrations/introduction"
|
||||
>
|
||||
View the Integration docs page for more information on connecting an API
|
||||
using an Integration or other method.
|
||||
@@ -255,6 +321,101 @@ export function HowToUseThisIntegration({
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToUseApiKeysAndEndpoints() {
|
||||
return (
|
||||
<>
|
||||
<Paragraph spacing>
|
||||
Environments and Endpoints are used to connect your server to the
|
||||
Trigger.dev platform.
|
||||
</Paragraph>
|
||||
<Header2 spacing>Environments</Header2>
|
||||
<Paragraph spacing>
|
||||
Each environment has an API Key associated with it. This API Key is used
|
||||
to authenticate your Jobs with the Trigger.dev platform.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
The API Key you use for your{" "}
|
||||
<TextLink to="https://trigger.dev/docs/documentation/concepts/client-adaptors">
|
||||
Client
|
||||
</TextLink>{" "}
|
||||
is how we know which environment to run your code against:
|
||||
</Paragraph>
|
||||
<CodeBlock
|
||||
showLineNumbers={false}
|
||||
className="mb-4"
|
||||
code={`export const client = new TriggerClient({
|
||||
id: "nextjs-example",
|
||||
//this environment variable should be set to your DEV API Key locally,
|
||||
//and your PROD API Key in production
|
||||
apiKey: process.env.TRIGGER_API_KEY!,
|
||||
});`}
|
||||
/>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Development</span>
|
||||
<EnvironmentLabel environment={{ type: "DEVELOPMENT" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
The <InlineCode>DEV</InlineCode> environment should only be used for
|
||||
local development. It’s where you can test your Jobs before deploying
|
||||
them to servers.
|
||||
</Paragraph>
|
||||
<Callout variant="warning" className="my-2">
|
||||
Scheduled Triggers do not trigger Jobs in the DEV Environment. When
|
||||
you’re working locally you should use the Test feature to trigger any
|
||||
scheduled Jobs.
|
||||
</Callout>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="→"
|
||||
title={
|
||||
<span className="flex items-center gap-x-2">
|
||||
<span>Production</span>
|
||||
<EnvironmentLabel environment={{ type: "PRODUCTION" }} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
The <InlineCode>PROD</InlineCode> environment is where your Jobs will
|
||||
run in production. It’s where you can run your Jobs against real data.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<Header2 spacing>Endpoints</Header2>
|
||||
<Paragraph spacing>
|
||||
An Endpoint is a URL on your server that Trigger.dev can connect to.
|
||||
This URL is used to register Jobs, start them and orchestrate runs and
|
||||
retries.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
<InlineCode>DEV</InlineCode> has multiple endpoints associated with it –
|
||||
one for each team member. This allows each team member to run their own
|
||||
Jobs, without interfering with each other.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
All other environments have just a single endpoint (with a single URL)
|
||||
associated with them.
|
||||
</Paragraph>
|
||||
<Header2 spacing>Deployment</Header2>
|
||||
<Paragraph spacing>
|
||||
Deployment uses Environments and Endpoints to connect your Jobs to the
|
||||
Trigger.dev platform.
|
||||
</Paragraph>
|
||||
<Callout
|
||||
variant="docs"
|
||||
to="https://trigger.dev/docs/documentation/guides/deployment"
|
||||
>
|
||||
Read the deployment guide to learn more.
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StepContentContainer({ children }: { children: React.ReactNode }) {
|
||||
return <div className="mb-6 ml-9 mt-1">{children}</div>;
|
||||
}
|
||||
|
||||
@@ -31,9 +31,8 @@ export function ApiKeyHelp({
|
||||
keys won't leave your server, we'll never see them.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
First install the{" "}
|
||||
<InlineCode>{integration.packageName}@next</InlineCode> package using
|
||||
your preferred package manager. For example:
|
||||
First install the <InlineCode>{integration.packageName}</InlineCode>{" "}
|
||||
package using your preferred package manager. For example:
|
||||
</Paragraph>
|
||||
<HelpInstall packageName={integration.packageName} />
|
||||
{help && (
|
||||
|
||||
@@ -21,21 +21,21 @@ export function HelpInstall({ packageName }: { packageName: string }) {
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`npm install ${packageName}@next`}
|
||||
value={`npm install ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`pnpm install ${packageName}@next`}
|
||||
value={`pnpm install ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`yarn add ${packageName}@next`}
|
||||
value={`yarn add ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { runStatusTitle } from "../runs/RunStatuses";
|
||||
import { ProjectJob, useProject } from "~/hooks/useProject";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function JobsTable({
|
||||
jobs,
|
||||
@@ -46,7 +47,12 @@ export function JobsTable({
|
||||
jobs.map((job) => {
|
||||
const path = jobPath(organization, project, job);
|
||||
return (
|
||||
<TableRow key={job.id}>
|
||||
<TableRow
|
||||
key={job.id}
|
||||
className={cn(
|
||||
job.hasIntegrationsRequiringAction && "bg-rose-500/30"
|
||||
)}
|
||||
>
|
||||
<TableCell to={path}>
|
||||
<span className="flex items-center gap-2">
|
||||
<NamedIcon name={job.event.icon} className="h-8 w-8" />
|
||||
@@ -79,12 +85,30 @@ export function JobsTable({
|
||||
<SimpleTooltip
|
||||
key={integration.key}
|
||||
button={
|
||||
<NamedIcon
|
||||
name={integration.icon}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
<div className="relative">
|
||||
<NamedIcon
|
||||
name={integration.icon}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
{integration.setupStatus === "MISSING_FIELDS" && (
|
||||
<NamedIcon
|
||||
name="error"
|
||||
className="absolute -left-1 -top-1 h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
content={
|
||||
<div>
|
||||
<p className="mb-1 text-rose-400">
|
||||
{integration.setupStatus === "MISSING_FIELDS" &&
|
||||
"This integration requires configuration"}
|
||||
</p>
|
||||
<p>
|
||||
{integration.title}: {integration.key}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
content={`${integration.title}: ${integration.key}`}
|
||||
/>
|
||||
))}
|
||||
</TableCell>
|
||||
|
||||
@@ -102,73 +102,77 @@ function BreadcrumbItem({
|
||||
case "projects":
|
||||
return <ProjectsMenu key={breadcrumb.slug} />;
|
||||
case "jobs":
|
||||
if (!organization || !project) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectPath(organization!, project!)}
|
||||
to={projectPath(organization, project)}
|
||||
title="Jobs"
|
||||
/>
|
||||
);
|
||||
case "environments":
|
||||
if (!organization || !project) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectEnvironmentsPath(organization!, project!)}
|
||||
to={projectEnvironmentsPath(organization, project)}
|
||||
title="Environments"
|
||||
/>
|
||||
);
|
||||
case "integrations":
|
||||
if (!organization || !project) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={projectIntegrationsPath(organization!, project!)}
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
title="Integrations"
|
||||
/>
|
||||
);
|
||||
case "integration":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={projectIntegrationsPath(organization!, project!)}
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
title="Integrations"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization!, project!, client!)}
|
||||
title={client!.title}
|
||||
to={integrationClientPath(organization, project, client)}
|
||||
title={client.title}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
case "integration-job":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientPath(organization!, project!, client!)}
|
||||
to={integrationClientPath(organization, project, client)}
|
||||
title={"Jobs"}
|
||||
/>
|
||||
);
|
||||
case "integration-connections":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientConnectionsPath(
|
||||
organization!,
|
||||
project!,
|
||||
client!
|
||||
)}
|
||||
to={integrationClientConnectionsPath(organization, project, client)}
|
||||
title={"Connections"}
|
||||
/>
|
||||
);
|
||||
case "integration-scopes":
|
||||
if (!organization || !project || !client) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
to={integrationClientScopesPath(organization!, project!, client!)}
|
||||
to={integrationClientScopesPath(organization, project, client)}
|
||||
title={"Scopes"}
|
||||
/>
|
||||
);
|
||||
case "job":
|
||||
if (!organization || !project || !job) return null;
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={projectPath(organization!, project!)}
|
||||
to={projectPath(organization, project)}
|
||||
title="Jobs"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
@@ -176,32 +180,35 @@ function BreadcrumbItem({
|
||||
</Fragment>
|
||||
);
|
||||
case "test":
|
||||
if (!organization || !project || !job) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={jobTestPath(organization!, project!, job!)}
|
||||
to={jobTestPath(organization, project, job)}
|
||||
title="Test"
|
||||
/>
|
||||
);
|
||||
case "runs":
|
||||
if (!organization || !project || !job) return null;
|
||||
return (
|
||||
<BreadcrumbLink
|
||||
key={breadcrumb.slug}
|
||||
to={jobPath(organization!, project!, job!)}
|
||||
to={jobPath(organization, project, job)}
|
||||
title="Runs"
|
||||
/>
|
||||
);
|
||||
case "run":
|
||||
if (!organization || !project || !job || !run) return null;
|
||||
return (
|
||||
<Fragment key={breadcrumb.slug}>
|
||||
<BreadcrumbLink
|
||||
to={jobPath(organization!, project!, job!)}
|
||||
to={jobPath(organization, project, job)}
|
||||
title="Runs"
|
||||
/>
|
||||
<BreadcrumbIcon />
|
||||
<BreadcrumbLink
|
||||
to={runDashboardPath(organization!, project!, job!, run!)}
|
||||
title={`Run #${run!.number}`}
|
||||
to={runDashboardPath(organization, project, job, run)}
|
||||
title={`Run #${run.number}`}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { motion } from "framer-motion";
|
||||
import invariant from "tiny-invariant";
|
||||
import { useJob, useOptionalJob } from "~/hooks/useJob";
|
||||
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
|
||||
import { useOptionalJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { cn } from "~/utils/cn";
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
projectIntegrationsPath,
|
||||
projectPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { NavLinkButton } from "../primitives/Buttons";
|
||||
import type { IconNames } from "../primitives/NamedIcon";
|
||||
import { SimpleTooltip } from "../primitives/Tooltip";
|
||||
import { UserProfilePhoto } from "../UserProfilePhoto";
|
||||
import { useOptionalIntegrationClient } from "~/hooks/useIntegrationClient";
|
||||
|
||||
export function SideMenuContainer({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -72,18 +71,21 @@ export function ProjectSideMenu() {
|
||||
to={projectPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
forceActive={jobsActive}
|
||||
data-action="jobs"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Integrations"
|
||||
icon="integration"
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="integrations"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Environments & API Keys"
|
||||
icon="environment"
|
||||
to={projectEnvironmentsPath(organization, project)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="environments & api keys"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@@ -92,18 +94,21 @@ export function ProjectSideMenu() {
|
||||
icon="team"
|
||||
to={organizationTeamPath(organization)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="team"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Usage & Billing"
|
||||
icon="billing"
|
||||
to={organizationBillingPath(organization)}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="usage & billing"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Account"
|
||||
icon={UserProfilePhoto}
|
||||
to={accountPath()}
|
||||
isCollapsed={isCollapsed}
|
||||
data-action="account"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -4,49 +4,56 @@ import {
|
||||
InformationCircleIcon,
|
||||
LightBulbIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BookOpenIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronRightIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const variantClasses = {
|
||||
export const variantClasses = {
|
||||
info: {
|
||||
className: "border-blue-400/20 bg-blue-400/30",
|
||||
icon: <InformationCircleIcon className="h-5 w-5 shrink-0 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
textColor: "text-blue-200",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
warning: {
|
||||
className: "border-yellow-400/20 bg-yellow-400/30",
|
||||
icon: (
|
||||
<ExclamationCircleIcon className="h-5 w-5 shrink-0 text-yellow-400" />
|
||||
),
|
||||
text: "text-yellow-200",
|
||||
textColor: "text-yellow-200",
|
||||
linkClassName: "transition hover:bg-yellow-400/40",
|
||||
},
|
||||
error: {
|
||||
className: "border-[hsl(347,77%,50%)]/30 bg-[hsl(347,77%,50%)]/20",
|
||||
className: "border-rose-500/20 bg-rose-500/30",
|
||||
icon: (
|
||||
<ExclamationTriangleIcon className="h-5 w-5 shrink-0 text-rose-400" />
|
||||
),
|
||||
text: "text-rose-200",
|
||||
textColor: "text-rose-200",
|
||||
linkClassName: "transition hover:bg-rose-500/40",
|
||||
},
|
||||
idea: {
|
||||
className: "border-green-400/20 bg-green-400/30",
|
||||
icon: <LightBulbIcon className="h-5 w-5 shrink-0 text-green-400" />,
|
||||
text: "text-green-200",
|
||||
textColor: "text-green-200",
|
||||
linkClassName: "transition hover:bg-green-400/40",
|
||||
},
|
||||
success: {
|
||||
className: "border-green-400/20 bg-green-400/30",
|
||||
icon: <CheckCircleIcon className="h-5 w-5 shrink-0 text-green-400" />,
|
||||
text: "text-green-200",
|
||||
textColor: "text-green-200",
|
||||
linkClassName: "transition hover:bg-green-400/40",
|
||||
},
|
||||
docs: {
|
||||
className:
|
||||
"border-blue-400/20 bg-blue-400/30 transition hover:bg-blue-400/40",
|
||||
className: "border-blue-400/20 bg-blue-400/30",
|
||||
icon: <BookOpenIcon className="mt-0.5 h-5 w-5 shrink-0 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
textColor: "text-blue-200",
|
||||
linkClassName: "transition hover:bg-blue-400/40",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -55,41 +62,79 @@ export function Callout({
|
||||
className,
|
||||
icon,
|
||||
variant,
|
||||
href,
|
||||
to,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
icon?: React.ReactNode;
|
||||
variant: keyof typeof variantClasses;
|
||||
href?: string;
|
||||
to?: string;
|
||||
}) {
|
||||
const variantDefinition = variantClasses[variant];
|
||||
|
||||
if (variant === "docs") {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-start gap-x-2">
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
if (to !== undefined) {
|
||||
if (to.startsWith("http")) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
variantDefinition.linkClassName,
|
||||
className
|
||||
)}
|
||||
</div>
|
||||
<ArrowTopRightOnSquareIcon className="h-5 w-5 text-blue-400" />
|
||||
</a>
|
||||
);
|
||||
>
|
||||
<div className={"flex w-full items-start gap-x-2"}>
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph
|
||||
variant={"small"}
|
||||
className={variantDefinition.textColor}
|
||||
>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
<ArrowTopRightOnSquareIcon
|
||||
className={cn("h-5 w-5", variantDefinition.textColor)}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={cn(
|
||||
`flex w-full items-start justify-between gap-2.5 rounded-md border py-2 pl-2 pr-3 shadow-md backdrop-blur-sm`,
|
||||
variantDefinition.className,
|
||||
variantDefinition.linkClassName,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={"flex w-full items-start gap-x-2"}>
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph
|
||||
variant={"small"}
|
||||
className={variantDefinition.textColor}
|
||||
>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
className={cn("h-5 w-5", variantDefinition.textColor)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -103,7 +148,7 @@ export function Callout({
|
||||
{icon ? icon : variantDefinition.icon}
|
||||
|
||||
{typeof children === "string" ? (
|
||||
<Paragraph variant={"small"} className={variantDefinition.text}>
|
||||
<Paragraph variant={"small"} className={variantDefinition.textColor}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
) : (
|
||||
|
||||
@@ -1,40 +1,96 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const header1Styles =
|
||||
"font-sans text-base md:text-lg lg:text-xl leading-5 md:leading-6 lg:leading-7 font-semibold";
|
||||
const header2Styles = "font-sans text-base leading-6 font-medium text-bright";
|
||||
const header3Styles = "font-sans text-sm leading-5 font-medium text-bright";
|
||||
const headerVariants = {
|
||||
header1: {
|
||||
text: "font-sans text-base md:text-lg lg:text-xl leading-5 md:leading-6 lg:leading-7 font-semibold",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
header2: {
|
||||
text: "font-sans text-base leading-6 font-medium",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
header3: {
|
||||
text: "font-sans text-sm leading-5 font-medium",
|
||||
spacing: "mb-2",
|
||||
},
|
||||
};
|
||||
|
||||
const header1Variants = {
|
||||
default: "text-bright",
|
||||
const textColorVariants = {
|
||||
bright: "text-bright",
|
||||
dimmed: "text-dimmed",
|
||||
};
|
||||
|
||||
export type HeaderVariant = keyof typeof headerVariants;
|
||||
|
||||
type HeaderProps = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type Header1Props = HeaderProps & {
|
||||
variant?: keyof typeof header1Variants;
|
||||
};
|
||||
spacing?: boolean;
|
||||
textColor?: "bright" | "dimmed";
|
||||
} & React.HTMLAttributes<HTMLHeadingElement>;
|
||||
|
||||
export function Header1({
|
||||
variant = "default",
|
||||
className,
|
||||
children,
|
||||
}: Header1Props) {
|
||||
spacing = false,
|
||||
textColor = "bright",
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<h1 className={cn(header1Styles, header1Variants[variant], className)}>
|
||||
<h1
|
||||
className={cn(
|
||||
headerVariants.header1.text,
|
||||
spacing === true && headerVariants.header1.spacing,
|
||||
textColor === "bright"
|
||||
? textColorVariants.bright
|
||||
: textColorVariants.dimmed,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header2({ className, children }: HeaderProps) {
|
||||
return <h2 className={cn(header2Styles, className)}>{children}</h2>;
|
||||
export function Header2({
|
||||
className,
|
||||
children,
|
||||
spacing = false,
|
||||
textColor = "bright",
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<h2
|
||||
className={cn(
|
||||
headerVariants.header2.text,
|
||||
spacing === true && headerVariants.header2.spacing,
|
||||
textColor === "bright"
|
||||
? textColorVariants.bright
|
||||
: textColorVariants.dimmed,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header3({ className, children }: HeaderProps) {
|
||||
return <h3 className={cn(header3Styles, className)}>{children}</h3>;
|
||||
export function Header3({
|
||||
className,
|
||||
children,
|
||||
spacing = false,
|
||||
textColor = "bright",
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
headerVariants.header3.text,
|
||||
spacing === true && headerVariants.header3.spacing,
|
||||
textColor === "bright"
|
||||
? textColorVariants.bright
|
||||
: textColorVariants.dimmed,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function HelpTrigger({ title }: { title: string }) {
|
||||
<Button
|
||||
variant="tertiary/small"
|
||||
LeadingIcon="lightbulb"
|
||||
leadingIconClassName="text-slate-400"
|
||||
leadingIconClassName="text-amber-400"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import type { IconNames } from "./NamedIcon";
|
||||
import type { IconNamesOrString } from "./NamedIcon";
|
||||
import { NamedIcon } from "./NamedIcon";
|
||||
|
||||
const variants = {
|
||||
@@ -44,7 +44,7 @@ const variants = {
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
variant?: keyof typeof variants;
|
||||
icon?: IconNames;
|
||||
icon?: IconNamesOrString;
|
||||
shortcut?: string;
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Header2, Header3 } from "./Headers";
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
const labelVariants = {
|
||||
small: {
|
||||
text: "font-sans text-sm font-normal",
|
||||
},
|
||||
medium: {
|
||||
text: "font-sans text-sm leading-5 font-medium",
|
||||
},
|
||||
large: {
|
||||
text: "font-sans text-base leading-6 font-medium",
|
||||
},
|
||||
};
|
||||
|
||||
type LabelProps = React.AllHTMLAttributes<HTMLLabelElement> & {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
variant?: "small" | "medium" | "large";
|
||||
variant?: keyof typeof labelVariants;
|
||||
};
|
||||
|
||||
export function Label({
|
||||
@@ -15,17 +25,10 @@ export function Label({
|
||||
variant = "medium",
|
||||
...props
|
||||
}: LabelProps) {
|
||||
return variant === "medium" ? (
|
||||
<Header3 className={cn(className)} {...props}>
|
||||
const variation = labelVariants[variant];
|
||||
return (
|
||||
<label className={cn(variation.text, className)} {...props}>
|
||||
{children}
|
||||
</Header3>
|
||||
) : variant === "large" ? (
|
||||
<Header2 className={cn(className)} {...props}>
|
||||
{children}
|
||||
</Header2>
|
||||
) : (
|
||||
<Paragraph variant="small" className={cn(className)}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
ArrowLeftIcon,
|
||||
ArrowPathIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BeakerIcon,
|
||||
BellAlertIcon,
|
||||
BoltIcon,
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
MagnifyingGlassIcon,
|
||||
PlusIcon,
|
||||
PlusSmallIcon,
|
||||
QrCodeIcon,
|
||||
SquaresPlusIcon,
|
||||
StarIcon,
|
||||
UserCircleIcon,
|
||||
@@ -103,7 +104,7 @@ const icons = {
|
||||
<XMarkIcon className={cn("text-dimmed", className)} />
|
||||
),
|
||||
"connection-alert": (className: string) => (
|
||||
<BellAlertIcon className={cn("text-rose-500", className)} />
|
||||
<BellAlertIcon className={cn("text-amber-500", className)} />
|
||||
),
|
||||
docs: (className: string) => (
|
||||
<BookOpenIcon className={cn("text-slate-400", className)} />
|
||||
@@ -175,6 +176,9 @@ const icons = {
|
||||
property: (className: string) => (
|
||||
<Cog8ToothIcon className={cn("text-slate-600", className)} />
|
||||
),
|
||||
"qr-code": (className: string) => (
|
||||
<QrCodeIcon className={cn("text-amber-400", className)} />
|
||||
),
|
||||
refresh: (className: string) => (
|
||||
<ArrowPathIcon className={cn("text-bright", className)} />
|
||||
),
|
||||
@@ -224,6 +228,7 @@ const icons = {
|
||||
};
|
||||
|
||||
export type IconNames = keyof typeof icons;
|
||||
export type IconNamesOrString = IconNames | (string & {});
|
||||
export const iconNames = Object.keys(icons) as IconNames[];
|
||||
|
||||
export function NamedIcon({
|
||||
@@ -231,7 +236,7 @@ export function NamedIcon({
|
||||
className,
|
||||
fallback,
|
||||
}: {
|
||||
name: IconNames | (string & {});
|
||||
name: IconNamesOrString;
|
||||
className: string;
|
||||
fallback?: JSX.Element;
|
||||
}) {
|
||||
@@ -241,7 +246,7 @@ export function NamedIcon({
|
||||
|
||||
if (hasIcon(name)) {
|
||||
return (
|
||||
<div
|
||||
<span
|
||||
className={cn(
|
||||
"grid aspect-square min-h-fit place-items-center",
|
||||
className
|
||||
@@ -255,7 +260,7 @@ export function NamedIcon({
|
||||
shapeRendering: "geometricPrecision",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
|
||||
>
|
||||
<ChevronLeftIcon className="h-6" />
|
||||
<Header1
|
||||
variant="dimmed"
|
||||
textColor="dimmed"
|
||||
className="transition group-hover:text-white"
|
||||
>
|
||||
{backButton.text}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { IconNamesOrString, NamedIcon } from "./NamedIcon";
|
||||
|
||||
const paragraphVariants = {
|
||||
base: {
|
||||
@@ -90,18 +92,47 @@ export function Paragraph({
|
||||
}
|
||||
|
||||
type TextLinkProps = {
|
||||
href: string;
|
||||
href?: string;
|
||||
to?: string;
|
||||
className?: string;
|
||||
trailingIcon?: IconNamesOrString;
|
||||
trailingIconClassName?: string;
|
||||
children: React.ReactNode;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
export function TextLink({ href, children, ...props }: TextLinkProps) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="text-indigo-500 transition hover:text-indigo-400"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
const classes =
|
||||
"text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group";
|
||||
|
||||
export function TextLink({
|
||||
href,
|
||||
to,
|
||||
children,
|
||||
className,
|
||||
trailingIcon,
|
||||
trailingIconClassName,
|
||||
...props
|
||||
}: TextLinkProps) {
|
||||
return to ? (
|
||||
<Link to={to} className={cn(classes, className)} {...props}>
|
||||
{children}{" "}
|
||||
{trailingIcon && (
|
||||
<NamedIcon
|
||||
name={trailingIcon}
|
||||
className={cn("h-4 w-4", trailingIconClassName)}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
) : href ? (
|
||||
<a href={href} className={cn(classes, className)} {...props}>
|
||||
{children}{" "}
|
||||
{trailingIcon && (
|
||||
<NamedIcon
|
||||
name={trailingIcon}
|
||||
className={cn("h-4 w-4", trailingIconClassName)}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
) : (
|
||||
<span>Need to define a path or href</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function StepNumber({
|
||||
stepNumber?: string;
|
||||
active?: boolean;
|
||||
complete?: boolean;
|
||||
title?: string;
|
||||
title?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -57,11 +57,11 @@ export function RunsTable({
|
||||
<TableBody>
|
||||
{total === 0 && !hasFilters ? (
|
||||
<TableBlankRow colSpan={8}>
|
||||
<NoRuns title="No runs found for this Job" />
|
||||
<NoRuns title="No Runs found for this Job" />
|
||||
</TableBlankRow>
|
||||
) : runs.length === 0 ? (
|
||||
<TableBlankRow colSpan={8}>
|
||||
<NoRuns title="No runs match your filters" />
|
||||
<NoRuns title="No Runs match your filters" />
|
||||
</TableBlankRow>
|
||||
) : (
|
||||
runs.map((run) => {
|
||||
|
||||
@@ -20,17 +20,44 @@ export const Callouts: Story = {
|
||||
|
||||
function CalloutsSet() {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-y-4 p-4">
|
||||
<Callout variant="info">This is an info callout</Callout>
|
||||
<Callout variant="warning">This is a warning callout</Callout>
|
||||
<Callout variant="error">This is an error callout</Callout>
|
||||
<Callout variant="idea">This is an idea callout</Callout>
|
||||
<Callout
|
||||
variant="idea"
|
||||
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
|
||||
>
|
||||
This has a custom icon
|
||||
</Callout>
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex flex-col items-start gap-y-4 p-4">
|
||||
<Callout variant="info">This is an info callout</Callout>
|
||||
<Callout variant="warning">This is a warning callout</Callout>
|
||||
<Callout variant="error">This is an error callout</Callout>
|
||||
<Callout variant="idea">This is an idea callout</Callout>
|
||||
<Callout variant="docs">This is a docs callout</Callout>
|
||||
<Callout
|
||||
variant="idea"
|
||||
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
|
||||
>
|
||||
This has a custom icon
|
||||
</Callout>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-y-4 p-4">
|
||||
<Callout to="#" variant="info">
|
||||
This is an info callout
|
||||
</Callout>
|
||||
<Callout to="#" variant="warning">
|
||||
This is a warning callout
|
||||
</Callout>
|
||||
<Callout to="#" variant="error">
|
||||
This is an error callout
|
||||
</Callout>
|
||||
<Callout to="#" variant="idea">
|
||||
This is an idea callout
|
||||
</Callout>
|
||||
<Callout to="#" variant="docs">
|
||||
This is a docs callout
|
||||
</Callout>
|
||||
<Callout
|
||||
to="#"
|
||||
variant="idea"
|
||||
icon={<EnvelopeIcon className="h-5 w-5 text-green-400" />}
|
||||
>
|
||||
This has a custom icon
|
||||
</Callout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,33 @@ function SearchForm() {
|
||||
fullWidth={false}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label variant="small">This is a small label</Label>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
required={true}
|
||||
icon="search"
|
||||
shortcut="⌘K"
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label variant="medium">This is a medium label</Label>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
required={true}
|
||||
icon="search"
|
||||
shortcut="⌘K"
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label variant="large">This is a large label</Label>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
required={true}
|
||||
icon="search"
|
||||
shortcut="⌘K"
|
||||
/>
|
||||
</InputGroup>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ function Typography({ header1, header2, header3, paragraph }: TypographyProps) {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<Header1>{header1}</Header1>
|
||||
<Header1 variant="dimmed">{header1}</Header1>
|
||||
<Header1 textColor="dimmed">{header1}</Header1>
|
||||
<Header2>{header2}</Header2>
|
||||
<Header3>{header3}</Header3>
|
||||
<Paragraph>{paragraph}</Paragraph>
|
||||
@@ -70,10 +70,14 @@ function Typography({ header1, header2, header3, paragraph }: TypographyProps) {
|
||||
TextLink. It takes an href and children.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn how to get started quickly using the included some example Jobs
|
||||
which are great as a quick start project. You can check them out in
|
||||
your project here in triggerdotdev/jobs/examples. You can also see the
|
||||
examples in more detail in the docs.
|
||||
Learn how to get{" "}
|
||||
<TextLink href="#" trailingIcon="external-link">
|
||||
started quickly
|
||||
</TextLink>{" "}
|
||||
using the included some example Jobs which are great as a quick start
|
||||
project. You can check them out in your project here in
|
||||
triggerdotdev/jobs/examples. You can also see the examples in more
|
||||
detail in the docs.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
|
||||
import { H } from "@highlight-run/node";
|
||||
import type { DataFunctionArgs, EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
|
||||
import { Response } from "@remix-run/node"; // or cloudflare/deno
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { parseAcceptLanguage } from "intl-parse-accept-language";
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
OperatingSystemContextProvider,
|
||||
OperatingSystemPlatform,
|
||||
} from "./components/primitives/OperatingSystemProvider";
|
||||
import { env } from "./env.server";
|
||||
|
||||
const ABORT_DELAY = 30000;
|
||||
|
||||
@@ -107,6 +109,7 @@ function serveBrowsers(
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
let shellReady = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<OperatingSystemContextProvider platform={platform}>
|
||||
<LocaleContextProvider locales={locales}>
|
||||
@@ -120,6 +123,7 @@ function serveBrowsers(
|
||||
{
|
||||
// use onShellReady to wait until a suspense boundary is triggered
|
||||
onShellReady() {
|
||||
shellReady = true;
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
let body = new PassThrough();
|
||||
pipe(body);
|
||||
@@ -133,9 +137,11 @@ function serveBrowsers(
|
||||
onShellError(err: unknown) {
|
||||
reject(err);
|
||||
},
|
||||
onError(err: unknown) {
|
||||
onError(error: unknown) {
|
||||
didError = true;
|
||||
console.error(err);
|
||||
if (shellReady) {
|
||||
logError(error, request);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -143,4 +149,35 @@ function serveBrowsers(
|
||||
});
|
||||
}
|
||||
|
||||
Worker.init().catch(console.error);
|
||||
if (env.HIGHLIGHT_PROJECT_ID) {
|
||||
H.init({ projectID: env.HIGHLIGHT_PROJECT_ID });
|
||||
}
|
||||
|
||||
export function handleError(
|
||||
error: unknown,
|
||||
{ request, params, context }: DataFunctionArgs
|
||||
) {
|
||||
logError(error, request);
|
||||
}
|
||||
|
||||
Worker.init().catch((error) => {
|
||||
logError(error);
|
||||
});
|
||||
|
||||
function logError(error: unknown, request?: Request) {
|
||||
if (env.HIGHLIGHT_PROJECT_ID) {
|
||||
const parsed = request
|
||||
? H.parseHeaders(Object.fromEntries(request.headers))
|
||||
: undefined;
|
||||
if (error instanceof Error) {
|
||||
H.consumeError(error, parsed?.secureSessionId, parsed?.requestId);
|
||||
} else {
|
||||
H.consumeError(
|
||||
new Error(`Unknown error: ${JSON.stringify(error)}`),
|
||||
parsed?.secureSessionId,
|
||||
parsed?.requestId
|
||||
);
|
||||
}
|
||||
}
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const EnvironmentSchema = z.object({
|
||||
.default(process.env.NODE_ENV),
|
||||
SECRET_STORE: SecretStoreOptionsSchema.default("DATABASE"),
|
||||
POSTHOG_PROJECT_KEY: z.string().optional(),
|
||||
HIGHLIGHT_PROJECT_ID: z.string().optional(),
|
||||
MAGIC_LINK_SECRET: z.string(),
|
||||
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
|
||||
@@ -12,7 +12,7 @@ export function featuresForRequest(request: Request): TriggerFeatures {
|
||||
const isManagedCloud =
|
||||
url.host === "cloud.trigger.dev" ||
|
||||
url.host === "test-cloud.trigger.dev" ||
|
||||
process.env.NODE_ENV === "development";
|
||||
process.env.CLOUD_ENV === "development";
|
||||
|
||||
return {
|
||||
isManagedCloud,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/** Call a function when the id of the item changes */
|
||||
export function useChanged<T extends { id: string }>(
|
||||
getItem: () => T | undefined,
|
||||
action: (item: T | undefined) => void,
|
||||
sendInitialUndefined = true
|
||||
) {
|
||||
const previousItemId = useRef<string | undefined>();
|
||||
const item = getItem();
|
||||
|
||||
//when the value changes, call the action
|
||||
useEffect(() => {
|
||||
if (previousItemId.current !== item?.id) {
|
||||
action(item);
|
||||
}
|
||||
|
||||
previousItemId.current = item?.id;
|
||||
}, [item]);
|
||||
|
||||
//if sendInitialUndefined is true, call the action when the component first renders
|
||||
useEffect(() => {
|
||||
if (item !== undefined || sendInitialUndefined === false) return;
|
||||
action(item);
|
||||
}, []);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { H } from "highlight.run";
|
||||
import { useUserChanged } from "./useUser";
|
||||
|
||||
export function useHighlight() {
|
||||
useUserChanged((user) => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
H.identify(user.id, {
|
||||
email: user.email,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
|
||||
import { useOptionalProject } from "./useProject";
|
||||
import { useChanged } from "./useChanged";
|
||||
|
||||
export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
|
||||
|
||||
@@ -31,3 +32,9 @@ export function useJob() {
|
||||
invariant(job, "Job must be defined");
|
||||
return job;
|
||||
}
|
||||
|
||||
export const useJobChanged = (
|
||||
action: (org: MatchedJob | undefined) => void
|
||||
) => {
|
||||
useChanged(useOptionalJob, action);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
|
||||
export type MatchedOrganization = UseDataFunctionReturn<
|
||||
typeof appLoader
|
||||
@@ -59,3 +60,9 @@ function useOrganizationsFromMatchesData(paths: string[]) {
|
||||
UseDataFunctionReturn<typeof appLoader>["organizations"]
|
||||
>(routeMatch.data.organizations);
|
||||
}
|
||||
|
||||
export const useOrganizationChanged = (
|
||||
action: (org: MatchedOrganization | undefined) => void
|
||||
) => {
|
||||
useChanged(useOptionalOrganization, action);
|
||||
};
|
||||
|
||||
@@ -1,62 +1,83 @@
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useOptionalOrganization } from "./useOrganizations";
|
||||
import { useOptionalUser } from "./useUser";
|
||||
import { useOrganizationChanged } from "./useOrganizations";
|
||||
import { useOptionalUser, useUserChanged } from "./useUser";
|
||||
import { useProjectChanged } from "./useProject";
|
||||
import { useJobChanged } from "./useJob";
|
||||
|
||||
export const usePostHog = (apiKey?: string, logging = false): void => {
|
||||
export const usePostHog = (
|
||||
apiKey?: string,
|
||||
logging = false,
|
||||
debug = false
|
||||
): void => {
|
||||
const postHogInitialized = useRef(false);
|
||||
const location = useLocation();
|
||||
const user = useOptionalUser();
|
||||
const currentOrganization = useOptionalOrganization();
|
||||
const currentOrganizationId = currentOrganization?.id;
|
||||
|
||||
//start PostHog once
|
||||
useEffect(() => {
|
||||
if (apiKey === undefined) return;
|
||||
if (apiKey === "") return;
|
||||
if (apiKey === undefined || apiKey === "") return;
|
||||
if (postHogInitialized.current === true) return;
|
||||
if (logging) console.log("posthog.init", apiKey);
|
||||
postHogInitialized.current = true;
|
||||
if (logging) console.log("Initializing PostHog");
|
||||
posthog.init(apiKey, {
|
||||
api_host: "https://app.posthog.com",
|
||||
opt_in_site_apps: true,
|
||||
debug: logging,
|
||||
debug,
|
||||
loaded: function (posthog) {
|
||||
if (logging) console.log("posthog.loaded", apiKey);
|
||||
if (logging) console.log("PostHog loaded");
|
||||
if (user !== undefined) {
|
||||
if (logging) console.log("posthog.identify", user.id, user.email);
|
||||
if (logging) console.log("Loaded: Identifying user", user);
|
||||
posthog.identify(user.id, { email: user.email });
|
||||
}
|
||||
},
|
||||
});
|
||||
postHogInitialized.current = true;
|
||||
}, [apiKey, logging, user]);
|
||||
|
||||
//identify the user, and unidentify on log out
|
||||
useEffect(() => {
|
||||
useUserChanged((user) => {
|
||||
if (postHogInitialized.current === false) return;
|
||||
if (user === undefined) {
|
||||
if (logging) console.log("posthog.reset");
|
||||
posthog.reset();
|
||||
} else {
|
||||
if (logging) console.log("posthog.identify", user.id, user.email);
|
||||
if (logging) console.log("User changed");
|
||||
if (user) {
|
||||
if (logging) console.log("Identifying user", user);
|
||||
posthog.identify(user.id, { email: user.email });
|
||||
} else {
|
||||
if (logging) console.log("Resetting user");
|
||||
posthog.reset();
|
||||
}
|
||||
}, [logging, user]);
|
||||
});
|
||||
|
||||
//identify the organization
|
||||
useEffect(() => {
|
||||
useOrganizationChanged((org) => {
|
||||
if (postHogInitialized.current === false) return;
|
||||
if (currentOrganizationId !== undefined) {
|
||||
if (logging) console.log("posthog.organization", currentOrganizationId);
|
||||
posthog.group("organization", currentOrganizationId);
|
||||
if (org) {
|
||||
if (logging) console.log(`Grouping by organization`, org);
|
||||
posthog.group("organization", org.id);
|
||||
} else {
|
||||
//reset the groups when you go to one of the top-level pages
|
||||
if (logging) console.log("Resetting groups");
|
||||
posthog.resetGroups();
|
||||
}
|
||||
}, [currentOrganizationId, logging]);
|
||||
});
|
||||
|
||||
useProjectChanged((project) => {
|
||||
if (postHogInitialized.current === false) return;
|
||||
if (project) {
|
||||
if (logging) console.log(`Grouping by project`, project);
|
||||
posthog.group("project", project.id);
|
||||
}
|
||||
});
|
||||
|
||||
useJobChanged((job) => {
|
||||
if (postHogInitialized.current === false) return;
|
||||
if (job) {
|
||||
if (logging) console.log(`Grouping by job`, job);
|
||||
posthog.group("job", job.id);
|
||||
}
|
||||
});
|
||||
|
||||
//page view
|
||||
useEffect(() => {
|
||||
if (postHogInitialized.current === false) return;
|
||||
if (logging) console.log("posthog.capture", "$pageview", location.pathname);
|
||||
posthog.capture("$pageview");
|
||||
}, [location, logging]);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam/route";
|
||||
import { useChanged } from "./useChanged";
|
||||
|
||||
export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
|
||||
|
||||
@@ -22,3 +23,9 @@ export function useProject() {
|
||||
invariant(project, "Project must be defined");
|
||||
return project;
|
||||
}
|
||||
|
||||
export const useProjectChanged = (
|
||||
action: (org: MatchedProject | undefined) => void
|
||||
) => {
|
||||
useChanged(useOptionalProject, action);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { User } from "~/models/user.server";
|
||||
import { useMatchesData } from "~/utils";
|
||||
import { useChanged } from "./useChanged";
|
||||
|
||||
function isUser(user: any): user is User {
|
||||
return user && typeof user === "object" && typeof user.email === "string";
|
||||
@@ -22,3 +23,7 @@ export function useUser(): User {
|
||||
}
|
||||
return maybeUser;
|
||||
}
|
||||
|
||||
export function useUserChanged(callback: (user: User | undefined) => void) {
|
||||
useChanged(useOptionalUser, callback);
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -11,3 +11,8 @@ interface Array<T> {
|
||||
thisArg?: any
|
||||
): Exclude<S, Falsy>[];
|
||||
}
|
||||
|
||||
// See this for more: https://twitter.com/mattpocockuk/status/1653403198885904387?s=20
|
||||
export type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { prisma } from "~/db.server";
|
||||
import { Prettify } from "~/lib.es5";
|
||||
|
||||
export type ExtendedEndpoint = Prettify<
|
||||
Awaited<ReturnType<typeof findEndpoint>>
|
||||
>;
|
||||
|
||||
export async function findEndpoint(id: string) {
|
||||
return await prisma.endpoint.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
environment: {
|
||||
include: {
|
||||
project: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -116,10 +116,6 @@ export async function createOrganization(
|
||||
userId,
|
||||
});
|
||||
|
||||
await workerQueue.enqueue("organizationCreated", {
|
||||
id: organization.id,
|
||||
});
|
||||
|
||||
return { ...organization, projects: [project] };
|
||||
}
|
||||
|
||||
|
||||
@@ -78,27 +78,72 @@ export async function findOrCreateGithubUser({
|
||||
? (authenticationExtraParams as unknown as Prisma.JsonObject)
|
||||
: undefined;
|
||||
|
||||
const fields = {
|
||||
accessToken,
|
||||
authenticationProfile: authProfile,
|
||||
authenticationExtraParams: authExtraParams,
|
||||
name,
|
||||
avatarUrl,
|
||||
displayName,
|
||||
};
|
||||
const authIdentifier = `github:${authenticationProfile.id}`;
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
authIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
const existingEmailUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingEmailUser && !existingUser) {
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
data: {
|
||||
accessToken,
|
||||
authenticationProfile: authProfile,
|
||||
authenticationExtraParams: authExtraParams,
|
||||
avatarUrl,
|
||||
authIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (existingEmailUser && existingUser) {
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: existingUser.id,
|
||||
},
|
||||
data: {
|
||||
accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: false,
|
||||
};
|
||||
}
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: {
|
||||
email,
|
||||
authIdentifier,
|
||||
},
|
||||
update: { accessToken },
|
||||
create: {
|
||||
accessToken,
|
||||
authenticationProfile: authProfile,
|
||||
authenticationExtraParams: authExtraParams,
|
||||
name,
|
||||
avatarUrl,
|
||||
displayName,
|
||||
authIdentifier,
|
||||
email,
|
||||
authenticationMethod: "GITHUB",
|
||||
},
|
||||
update: fields,
|
||||
create: { ...fields, email, authenticationMethod: "GITHUB" },
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -128,3 +173,22 @@ export function updateUser({
|
||||
data: { name, email, marketingEmails, confirmedBasicDetails: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function grantUserCloudAccess({
|
||||
id,
|
||||
inviteCode,
|
||||
}: {
|
||||
id: string;
|
||||
inviteCode: string;
|
||||
}) {
|
||||
return prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
invitationCode: {
|
||||
connect: {
|
||||
code: inviteCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from "graphile-worker";
|
||||
import { run as graphileRun } from "graphile-worker";
|
||||
|
||||
import { omit } from "lodash";
|
||||
import omit from "lodash.omit";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
@@ -57,6 +57,7 @@ export class ProjectPresenter {
|
||||
select: {
|
||||
slug: true,
|
||||
definition: true,
|
||||
setupStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -169,6 +170,7 @@ export class ProjectPresenter {
|
||||
key: integration.key,
|
||||
title: integration.integration.slug,
|
||||
icon: integration.integration.definition.id,
|
||||
setupStatus: integration.integration.setupStatus,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -197,6 +199,9 @@ export class ProjectPresenter {
|
||||
source: eventSpecification.source,
|
||||
},
|
||||
integrations,
|
||||
hasIntegrationsRequiringAction: integrations.some(
|
||||
(i) => i.setupStatus === "MISSING_FIELDS"
|
||||
),
|
||||
lastRun,
|
||||
properties,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
DisplayPropertiesSchema,
|
||||
DisplayProperty,
|
||||
DisplayPropertySchema,
|
||||
ErrorWithStack,
|
||||
ErrorWithStackSchema,
|
||||
EventSpecificationSchema,
|
||||
StyleSchema,
|
||||
} from "@trigger.dev/internal";
|
||||
import { z } from "zod";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
|
||||
type RunOptions = {
|
||||
id: string;
|
||||
@@ -35,6 +32,7 @@ const taskSelect = {
|
||||
delayUntil: true,
|
||||
description: true,
|
||||
properties: true,
|
||||
outputProperties: true,
|
||||
error: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
@@ -67,30 +65,15 @@ export class RunPresenter {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
//merge the properties from the version and the run, with the run properties taking precedence
|
||||
const mergedProperties = new Map<string, DisplayProperty>();
|
||||
if (run.version.properties) {
|
||||
const properties = DisplayPropertiesSchema.parse(run.version.properties);
|
||||
for (const property of properties) {
|
||||
mergedProperties.set(property.label, property);
|
||||
}
|
||||
}
|
||||
if (run.properties) {
|
||||
const properties = DisplayPropertiesSchema.parse(run.properties);
|
||||
for (const property of properties) {
|
||||
mergedProperties.set(property.label, property);
|
||||
}
|
||||
}
|
||||
if (run.version.eventSpecification) {
|
||||
const eventSpecification = EventSpecificationSchema.parse(
|
||||
run.version.eventSpecification
|
||||
);
|
||||
if (eventSpecification.properties) {
|
||||
for (const properties of eventSpecification.properties) {
|
||||
mergedProperties.set(properties.label, properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
const eventSpecification = EventSpecificationSchema.parse(
|
||||
run.version.eventSpecification
|
||||
);
|
||||
|
||||
const runProperties = mergeProperties(
|
||||
run.version.properties,
|
||||
run.properties,
|
||||
eventSpecification.properties
|
||||
);
|
||||
|
||||
const enrichTask = (task: QueryTask) => {
|
||||
const { children, ...t } = task;
|
||||
@@ -98,10 +81,7 @@ export class RunPresenter {
|
||||
...t,
|
||||
error: t.error ? ErrorWithStackSchema.parse(t.error) : undefined,
|
||||
connection: t.runConnection,
|
||||
properties:
|
||||
t.properties == null
|
||||
? []
|
||||
: z.array(DisplayPropertySchema).parse(t.properties),
|
||||
properties: mergeProperties(t.properties, t.outputProperties),
|
||||
style: t.style ? StyleSchema.parse(t.style) : undefined,
|
||||
};
|
||||
};
|
||||
@@ -147,7 +127,7 @@ export class RunPresenter {
|
||||
isTest: run.isTest,
|
||||
version: run.version.version,
|
||||
output: runOutput,
|
||||
properties: Array.from(mergedProperties.values()),
|
||||
properties: runProperties,
|
||||
environment: {
|
||||
type: run.environment.type,
|
||||
slug: run.environment.slug,
|
||||
@@ -212,14 +192,29 @@ export class RunPresenter {
|
||||
select: {
|
||||
...taskSelect,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DisplayPropertiesSchema, StyleSchema } from "@trigger.dev/internal";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { mergeProperties } from "~/utils/mergeProperties.server";
|
||||
|
||||
type DetailsProps = {
|
||||
id: string;
|
||||
@@ -56,6 +57,7 @@ export class TaskDetailsPresenter {
|
||||
noop: true,
|
||||
description: true,
|
||||
properties: true,
|
||||
outputProperties: true,
|
||||
params: true,
|
||||
output: true,
|
||||
error: true,
|
||||
@@ -89,10 +91,7 @@ export class TaskDetailsPresenter {
|
||||
...task,
|
||||
connection: task.runConnection,
|
||||
params: task.params as Record<string, any>,
|
||||
properties:
|
||||
task.properties == null
|
||||
? []
|
||||
: DisplayPropertiesSchema.parse(task.properties),
|
||||
properties: mergeProperties(task.properties, task.outputProperties),
|
||||
style: task.style ? StyleSchema.parse(task.style) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
+50
-62
@@ -7,8 +7,6 @@ import {
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
isRouteErrorResponse,
|
||||
useRouteError,
|
||||
} from "@remix-run/react";
|
||||
import {
|
||||
TypedMetaFunction,
|
||||
@@ -18,26 +16,27 @@ import {
|
||||
import type { ToastMessage } from "~/models/message.server";
|
||||
import { commitSession, getSession } from "~/models/message.server";
|
||||
import tailwindStylesheetUrl from "~/tailwind.css";
|
||||
import { RouteErrorDisplay } from "./components/ErrorDisplay";
|
||||
import { HighlightInit } from "./components/HighlightInit";
|
||||
import {
|
||||
AppContainer,
|
||||
MainCenteredContainer,
|
||||
} from "./components/layout/AppLayout";
|
||||
import { LinkButton } from "./components/primitives/Buttons";
|
||||
import { Header1, Header3 } from "./components/primitives/Headers";
|
||||
import { Toast } from "./components/primitives/Toast";
|
||||
import { env } from "./env.server";
|
||||
import { featuresForRequest } from "./features.server";
|
||||
import { usePostHog } from "./hooks/usePostHog";
|
||||
import { getUser } from "./services/session.server";
|
||||
import { appEnvTitleTag } from "./utils";
|
||||
import { friendlyErrorDisplay } from "./utils/httpErrors";
|
||||
import { ErrorBoundary as HighlightErrorBoundary } from "@highlight-run/react";
|
||||
import { useHighlight } from "./hooks/useHighlight";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
|
||||
};
|
||||
|
||||
export const meta: TypedMetaFunction<typeof loader> = ({ data }) => ({
|
||||
title: `Trigger.dev${appEnvTitleTag(data.appEnv)}`,
|
||||
title: `Trigger.dev${appEnvTitleTag(data?.appEnv)}`,
|
||||
charset: "utf-8",
|
||||
viewport: "width=device-width,initial-scale=1",
|
||||
});
|
||||
@@ -46,6 +45,7 @@ export const loader = async ({ request }: LoaderArgs) => {
|
||||
const session = await getSession(request.headers.get("cookie"));
|
||||
const toastMessage = session.get("toastMessage") as ToastMessage;
|
||||
const posthogProjectKey = env.POSTHOG_PROJECT_KEY;
|
||||
const highlightProjectId = env.HIGHLIGHT_PROJECT_ID;
|
||||
const features = featuresForRequest(request);
|
||||
|
||||
return typedjson(
|
||||
@@ -53,6 +53,7 @@ export const loader = async ({ request }: LoaderArgs) => {
|
||||
user: await getUser(request),
|
||||
toastMessage,
|
||||
posthogProjectKey,
|
||||
highlightProjectId,
|
||||
features,
|
||||
appEnv: env.APP_ENV,
|
||||
appOrigin: env.APP_ORIGIN,
|
||||
@@ -72,70 +73,57 @@ export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
|
||||
};
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="h-full overflow-hidden bg-darkBackground">
|
||||
<AppContainer showBackgroundGradient={true}>
|
||||
<MainCenteredContainer>
|
||||
{isRouteErrorResponse(error) ? (
|
||||
<ErrorDisplay
|
||||
title={
|
||||
friendlyErrorDisplay(error.status, error.statusText).title
|
||||
}
|
||||
message={
|
||||
error.data.message ??
|
||||
friendlyErrorDisplay(error.status, error.statusText).message
|
||||
}
|
||||
/>
|
||||
) : error instanceof Error ? (
|
||||
<ErrorDisplay title={error.name} message={error.message} />
|
||||
) : (
|
||||
<ErrorDisplay title="Oops" message={JSON.stringify(error)} />
|
||||
)}
|
||||
</MainCenteredContainer>
|
||||
</AppContainer>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorDisplay({ title, message }: { title: string; message?: string }) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Header1 className="mb-4 border-b border-slate-800 pb-4">{title}</Header1>
|
||||
{message && <Header3>{message}</Header3>}
|
||||
<LinkButton to="/" variant="primary/medium" className="mt-8">
|
||||
Home
|
||||
</LinkButton>
|
||||
</div>
|
||||
<>
|
||||
<html lang="en" className="h-full">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="h-full overflow-hidden bg-darkBackground">
|
||||
<AppContainer showBackgroundGradient={true}>
|
||||
<MainCenteredContainer>
|
||||
<RouteErrorDisplay />
|
||||
</MainCenteredContainer>
|
||||
</AppContainer>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { posthogProjectKey } = useTypedLoaderData<typeof loader>();
|
||||
const { posthogProjectKey, highlightProjectId } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
usePostHog(posthogProjectKey);
|
||||
useHighlight();
|
||||
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="h-full overflow-hidden bg-darkBackground">
|
||||
<Outlet />
|
||||
<Toast />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
<>
|
||||
{highlightProjectId && (
|
||||
<HighlightInit
|
||||
projectId={highlightProjectId}
|
||||
tracingOrigins={true}
|
||||
networkRecording={{ enabled: true, recordHeadersAndBody: true }}
|
||||
/>
|
||||
)}
|
||||
<html lang="en" className="h-full">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="h-full overflow-hidden bg-darkBackground">
|
||||
<HighlightErrorBoundary>
|
||||
<Outlet />
|
||||
</HighlightErrorBoundary>
|
||||
<Toast />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+16
-5
@@ -21,7 +21,7 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import useWindowSize from "react-use/lib/useWindowSize";
|
||||
import { docsPath } from "~/utils/pathBuilder";
|
||||
import { docsPath, projectIntegrationsPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: {
|
||||
@@ -83,6 +83,19 @@ export default function Page() {
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{project.jobs.length > 0 &&
|
||||
project.jobs.some(
|
||||
(j) => j.hasIntegrationsRequiringAction
|
||||
) && (
|
||||
<Callout
|
||||
variant="error"
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
className="mb-2"
|
||||
>
|
||||
Some of your Jobs have Integrations that have not been
|
||||
configured.
|
||||
</Callout>
|
||||
)}
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
{project.jobs.length === 0 ? (
|
||||
<Header2>Jobs</Header2>
|
||||
@@ -118,9 +131,7 @@ export default function Page() {
|
||||
{project.jobs.length === 1 ? (
|
||||
<Callout
|
||||
variant="docs"
|
||||
href={docsPath(
|
||||
"documentation/quickstart#your-first-job"
|
||||
)}
|
||||
to={docsPath("documentation/quickstart#your-first-job")}
|
||||
className="my-3"
|
||||
>
|
||||
Create your first Job in code
|
||||
@@ -128,7 +139,7 @@ export default function Page() {
|
||||
) : (
|
||||
<Callout
|
||||
variant="docs"
|
||||
href={docsPath("documentation/guides/create-a-job")}
|
||||
to={docsPath("documentation/guides/create-a-job")}
|
||||
className="my-3"
|
||||
>
|
||||
Create another Job
|
||||
|
||||
+92
-65
@@ -9,7 +9,7 @@ import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header1, Header2 } from "~/components/primitives/Headers";
|
||||
import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import {
|
||||
PageDescription,
|
||||
PageHeader,
|
||||
@@ -36,6 +36,12 @@ import { ProjectParamSchema } from "~/utils/pathBuilder";
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
HowToSetupYourProject,
|
||||
HowToUseApiKeysAndEndpoints,
|
||||
} from "~/components/helpContent/HelpContentText";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -105,73 +111,94 @@ export default function Page() {
|
||||
</PageDescription>
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<Header1>API Keys</Header1>
|
||||
<div className="mb-8 mt-4 flex gap-4">
|
||||
{environments.map((environment) => (
|
||||
<ClipboardField
|
||||
key={environment.id}
|
||||
fullWidth={false}
|
||||
secure
|
||||
value={environment.apiKey}
|
||||
variant={"primary/medium"}
|
||||
icon={<EnvironmentLabel environment={environment} />}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Help defaultOpen>
|
||||
{(open) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full gap-4",
|
||||
open ? "grid-cols-2" : "grid-cols-1"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<Header2>API Keys</Header2>
|
||||
<HelpTrigger title="How do I use API Keys and Endpoints?" />
|
||||
</div>
|
||||
<div className="mb-8 mt-4 flex gap-4">
|
||||
{environments.map((environment) => (
|
||||
<ClipboardField
|
||||
key={environment.id}
|
||||
fullWidth={false}
|
||||
secure
|
||||
value={environment.apiKey}
|
||||
variant={"primary/medium"}
|
||||
icon={<EnvironmentLabel environment={environment} />}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Header1 className="mb-2">Endpoints</Header1>
|
||||
<div className="flex flex-col gap-4">
|
||||
{clients.length > 0 ? (
|
||||
clients.map((client) => (
|
||||
<div key={client.slug}>
|
||||
<Header2 className="mb-2">{client.slug}</Header2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Environment</TableHeaderCell>
|
||||
<TableHeaderCell>Url</TableHeaderCell>
|
||||
<TableHeaderCell>Last refreshed</TableHeaderCell>
|
||||
<TableHeaderCell>Jobs</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.DEVELOPMENT}
|
||||
type="DEVELOPMENT"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "DEVELOPMENT",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.PRODUCTION}
|
||||
type="PRODUCTION"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "PRODUCTION",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Header2 className="mb-2">Endpoints</Header2>
|
||||
<div className="flex flex-col gap-4">
|
||||
{clients.length > 0 ? (
|
||||
clients.map((client) => (
|
||||
<div key={client.slug}>
|
||||
<Header3 className="mb-2">{client.slug}</Header3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Environment</TableHeaderCell>
|
||||
<TableHeaderCell>Url</TableHeaderCell>
|
||||
<TableHeaderCell>Last refreshed</TableHeaderCell>
|
||||
<TableHeaderCell>Jobs</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>
|
||||
Go to page
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.DEVELOPMENT}
|
||||
type="DEVELOPMENT"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "DEVELOPMENT",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<EndpointRow
|
||||
endpoint={client.endpoints.PRODUCTION}
|
||||
type="PRODUCTION"
|
||||
onClick={() =>
|
||||
setSelected({
|
||||
client: client.slug,
|
||||
type: "PRODUCTION",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<Paragraph>You have no clients yet</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
{selectedEndpoint && (
|
||||
<ConfigureEndpointSheet
|
||||
slug={selectedEndpoint.clientSlug}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
type={selectedEndpoint.type}
|
||||
onClose={() => setSelected(undefined)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<Paragraph>You have no clients yet</Paragraph>
|
||||
<HelpContent title="How to use API Keys and Endpoints">
|
||||
<HowToUseApiKeysAndEndpoints />
|
||||
</HelpContent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedEndpoint && (
|
||||
<ConfigureEndpointSheet
|
||||
slug={selectedEndpoint.clientSlug}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
type={selectedEndpoint.type}
|
||||
onClose={() => setSelected(undefined)}
|
||||
/>
|
||||
)}
|
||||
</Help>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
+1
-1
@@ -390,7 +390,7 @@ function IntegrationsWithMissingFields({
|
||||
<div className="mb-6">
|
||||
<Header2 className="mb-2 flex items-center gap-1">
|
||||
<NamedIcon name="error" className="h-5 w-5" />
|
||||
Integrations with missing details
|
||||
Integrations requiring configuration
|
||||
</Header2>
|
||||
|
||||
<Table>
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ export default function Page() {
|
||||
/>
|
||||
<Callout
|
||||
variant="docs"
|
||||
href={docsIntegrationPath(client.integration.identifier)}
|
||||
to={docsIntegrationPath(client.integration.identifier)}
|
||||
>
|
||||
View the docs to learn more about using the{" "}
|
||||
{client.integration.name} Integration.
|
||||
|
||||
+6
-4
@@ -24,10 +24,11 @@ function NextButton({ cursor }: { cursor?: string }) {
|
||||
return path ? (
|
||||
<LinkButton
|
||||
to={path}
|
||||
variant={"secondary/small"}
|
||||
variant={"tertiary/small"}
|
||||
TrailingIcon="chevron-right"
|
||||
className="flex items-center"
|
||||
>
|
||||
<span className="sr-only">Next</span>
|
||||
Next
|
||||
</LinkButton>
|
||||
) : null;
|
||||
}
|
||||
@@ -38,10 +39,11 @@ function PreviousButton({ cursor }: { cursor?: string }) {
|
||||
return path ? (
|
||||
<LinkButton
|
||||
to={path}
|
||||
variant={"secondary/small"}
|
||||
variant={"tertiary/small"}
|
||||
LeadingIcon="chevron-left"
|
||||
className="flex items-center"
|
||||
>
|
||||
<span className="sr-only">Previous</span>
|
||||
Prev
|
||||
</LinkButton>
|
||||
) : null;
|
||||
}
|
||||
|
||||
+54
-13
@@ -1,14 +1,21 @@
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { HowToRunYourJob } from "~/components/helpContent/HelpContentText";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { RunsTable } from "~/components/runs/RunsTable";
|
||||
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 } from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "./ListPagination";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { JobParamsSchema } from "~/utils/pathBuilder";
|
||||
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";
|
||||
|
||||
export const DirectionSchema = z.union([
|
||||
z.literal("forward"),
|
||||
@@ -54,17 +61,51 @@ export default function Page() {
|
||||
const { list } = useTypedLoaderData<typeof loader>();
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const job = useJob();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListPagination list={list} className="mb-1 justify-end" />
|
||||
<RunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={false}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<ListPagination list={list} className="mb-1 justify-end" />
|
||||
</div>
|
||||
<>
|
||||
{job.hasIntegrationsRequiringAction && (
|
||||
<Callout
|
||||
variant="error"
|
||||
to={projectIntegrationsPath(organization, project)}
|
||||
className="mb-2"
|
||||
>
|
||||
{simplur`This Job has ${
|
||||
job.integrations.filter((j) => j.setupStatus === "MISSING_FIELDS")
|
||||
.length
|
||||
} Integration[|s] that [has|have] not been configured.`}
|
||||
</Callout>
|
||||
)}
|
||||
<Help defaultOpen={list.runs.length === 0}>
|
||||
{(open) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-fit gap-4",
|
||||
open ? "grid-cols-2" : "grid-cols-1"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
<HelpTrigger title="How do I run my Job?" />
|
||||
</div>
|
||||
<RunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={false}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<ListPagination list={list} className="mt-2 justify-end" />
|
||||
</div>
|
||||
<HelpContent title="How to run your Job">
|
||||
<HowToRunYourJob />
|
||||
</HelpContent>
|
||||
</div>
|
||||
)}
|
||||
</Help>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-3
@@ -3,7 +3,6 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { useLocales } from "~/components/primitives/LocaleProvider";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
Table,
|
||||
@@ -58,7 +57,6 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
|
||||
export default function Page() {
|
||||
const { task } = useTypedLoaderData<typeof loader>();
|
||||
const locales = useLocales();
|
||||
|
||||
const {
|
||||
name,
|
||||
@@ -139,7 +137,7 @@ export default function Page() {
|
||||
{properties.length > 0 && (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Header3>Properties</Header3>
|
||||
<RunPanelProperties properties={properties} layout="vertical" />
|
||||
<RunPanelProperties properties={properties} layout="horizontal" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export default function Page() {
|
||||
<Header3>Properties</Header3>
|
||||
<RunPanelProperties
|
||||
properties={run.properties}
|
||||
layout="vertical"
|
||||
layout="horizontal"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export default function Page() {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="Job settings"
|
||||
description="Disable a job, archive it and more."
|
||||
description="Disable a Job, archive it and more."
|
||||
icon="settings"
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-2
@@ -178,7 +178,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Help defaultOpen={!hasTestRuns}>
|
||||
<Help defaultOpen={true}>
|
||||
{(open) => (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -254,7 +254,7 @@ export default function Page() {
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
<HelpTrigger title="How do I run a test" />
|
||||
<HelpTrigger title="How do I run a test?" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto rounded border border-slate-850 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<JSONEditor
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export default function Page() {
|
||||
return (
|
||||
<ComingSoon
|
||||
title="View all Triggers"
|
||||
description="View all of the registered Triggers for this job."
|
||||
description="View all of the registered Triggers for this Job."
|
||||
icon="trigger"
|
||||
/>
|
||||
);
|
||||
|
||||
+6
-3
@@ -43,7 +43,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
});
|
||||
|
||||
if (job === null) {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
throw new Response("Not Found", {
|
||||
status: 404,
|
||||
statusText: `There is no Job ${jobParam} in this Project.`,
|
||||
});
|
||||
}
|
||||
|
||||
//todo identify job
|
||||
@@ -111,7 +114,7 @@ export default function Job() {
|
||||
<PageInfoProperty
|
||||
label="Integrations"
|
||||
value={
|
||||
<div className="flex gap-0.5">
|
||||
<span className="flex gap-0.5">
|
||||
{job.integrations.map((integration, index) => (
|
||||
<NamedIcon
|
||||
key={index}
|
||||
@@ -119,7 +122,7 @@ export default function Job() {
|
||||
className={"h-4 w-4"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
+22
-1
@@ -2,14 +2,18 @@ import { Outlet } from "@remix-run/react";
|
||||
import type { LoaderArgs } 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 { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { ProjectPresenter } from "~/presenters/ProjectPresenter.server";
|
||||
import { analytics } from "~/services/analytics.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { projectPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -25,7 +29,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
throw new Response("Not Found", {
|
||||
status: 404,
|
||||
statusText: `Project ${projectParam} not found in your Organization.`,
|
||||
});
|
||||
}
|
||||
|
||||
analytics.project.identify({ project });
|
||||
@@ -34,6 +41,10 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
project,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Response) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
throw new Response(undefined, {
|
||||
status: 400,
|
||||
@@ -61,3 +72,13 @@ export default function Project() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const org = useOrganization();
|
||||
const project = useProject();
|
||||
return (
|
||||
<RouteErrorDisplay
|
||||
button={{ title: project.name, to: projectPath(org, project) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Outlet } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { getOrganizationFromSlug } from "~/models/organization.server";
|
||||
import { analytics } from "~/services/analytics.server";
|
||||
import {
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
setCurrentOrg,
|
||||
} from "~/services/currentOrganization.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { organizationPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -49,6 +52,15 @@ export default function Organization() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const org = useOrganization();
|
||||
return (
|
||||
<RouteErrorDisplay
|
||||
button={{ title: org.title, to: organizationPath(org) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
|
||||
if (options.formAction === "/resources/environment") {
|
||||
return false;
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
import { Outlet, ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
|
||||
import { ImpersonationBanner } from "~/components/ImpersonationBanner";
|
||||
import { NoMobileOverlay } from "~/components/NoMobileOverlay";
|
||||
import { AppContainer } from "~/components/layout/AppLayout";
|
||||
import {
|
||||
AppContainer,
|
||||
MainCenteredContainer,
|
||||
} from "~/components/layout/AppLayout";
|
||||
import { NavBar } from "~/components/navigation/NavBar";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { useIsProjectChildPage } from "~/hooks/useIsProjectChildPage";
|
||||
import { getOrganizations } from "~/models/organization.server";
|
||||
|
||||
import { getImpersonationId } from "~/services/impersonation.server";
|
||||
import { clearRedirectTo, commitSession } from "~/services/redirectTo.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { confirmBasicDetailsPath } from "~/utils/pathBuilder";
|
||||
import {
|
||||
confirmBasicDetailsPath,
|
||||
invitationCodePath,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const organizations = await getOrganizations({ userId: user.id });
|
||||
const impersonationId = await getImpersonationId(request);
|
||||
const features = featuresForRequest(request);
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (
|
||||
features.isManagedCloud &&
|
||||
!user.invitationCodeId &&
|
||||
url.pathname !== invitationCodePath()
|
||||
) {
|
||||
throw redirect(invitationCodePath());
|
||||
}
|
||||
|
||||
//you have to confirm basic details before you can do anything
|
||||
if (!user.confirmedBasicDetails) {
|
||||
@@ -78,3 +97,16 @@ export default function App() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<>
|
||||
<NoMobileOverlay />
|
||||
<AppContainer showBackgroundGradient={true}>
|
||||
<MainCenteredContainer>
|
||||
<RouteErrorDisplay />
|
||||
</MainCenteredContainer>
|
||||
</AppContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ export class CompleteRunTaskService {
|
||||
status: "COMPLETED",
|
||||
output: taskBody.output ?? undefined,
|
||||
completedAt: new Date(),
|
||||
outputProperties: taskBody.properties,
|
||||
},
|
||||
include: {
|
||||
attempts: true,
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import type { LoaderFunction } from "@remix-run/node";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { redirectCookie } from "./auth.github";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
export let loader: LoaderFunction = async ({ request }) => {
|
||||
const cookie = request.headers.get("Cookie");
|
||||
const redirectValue = await redirectCookie.parse(cookie);
|
||||
const redirectTo = redirectValue ?? "/";
|
||||
|
||||
logger.debug("auth.github.callback loader", {
|
||||
redirectTo,
|
||||
});
|
||||
|
||||
const authuser = await authenticator.authenticate("github", request, {
|
||||
successRedirect: redirectTo,
|
||||
failureRedirect: "/login",
|
||||
});
|
||||
|
||||
logger.debug("auth.github.callback authuser", {
|
||||
authuser,
|
||||
});
|
||||
|
||||
return authuser;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { ActionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
AppContainer,
|
||||
MainCenteredContainer,
|
||||
} from "~/components/layout/AppLayout";
|
||||
import { NavBar } from "~/components/navigation/NavBar";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Fieldset } from "~/components/primitives/Fieldset";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { FormTitle } from "~/components/primitives/FormTitle";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { TextLink } from "~/components/primitives/Paragraph";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { grantUserCloudAccess } from "~/models/user.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { organizationsPath } from "~/utils/pathBuilder";
|
||||
|
||||
function createSchema(
|
||||
constraints: {
|
||||
isValidCode?: (code: string) => Promise<boolean>;
|
||||
} = {}
|
||||
) {
|
||||
return z.object({
|
||||
code: z
|
||||
.string()
|
||||
.min(1, "Invite code missing")
|
||||
.superRefine((code, ctx) => {
|
||||
if (constraints.isValidCode === undefined) {
|
||||
//client-side validation skips this
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: conform.VALIDATION_UNDEFINED,
|
||||
});
|
||||
} else {
|
||||
// Tell zod this is an async validation by returning the promise
|
||||
return constraints.isValidCode(code).then((isValid) => {
|
||||
if (isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid invitation code",
|
||||
});
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const formData = await request.formData();
|
||||
|
||||
const formSchema = createSchema({
|
||||
isValidCode: async (code) => {
|
||||
const invitationCode = await prisma.invitationCode.findUnique({
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
});
|
||||
|
||||
return invitationCode !== undefined && invitationCode !== null;
|
||||
},
|
||||
});
|
||||
|
||||
const submission = await parse(formData, { schema: formSchema, async: true });
|
||||
|
||||
if (!submission.value) {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
try {
|
||||
await grantUserCloudAccess({
|
||||
id: userId,
|
||||
inviteCode: submission.value.code,
|
||||
});
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
organizationsPath(),
|
||||
request,
|
||||
"🚀 Welcome to the Trigger.dev Cloud private beta"
|
||||
);
|
||||
} catch (error: any) {
|
||||
return json({ errors: { body: error.message } }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const lastSubmission = useActionData();
|
||||
|
||||
const [form, { code }] = useForm({
|
||||
id: "invitation-code",
|
||||
lastSubmission,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: createSchema() });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AppContainer showBackgroundGradient={true}>
|
||||
<NavBar />
|
||||
<MainCenteredContainer>
|
||||
<FormTitle
|
||||
LeadingIcon="qr-code"
|
||||
title="Trigger.dev Cloud Beta"
|
||||
description={
|
||||
<>
|
||||
Enter your code to get access. If you don't have one you can
|
||||
always{" "}
|
||||
<TextLink
|
||||
target="_blank"
|
||||
href="https://trigger.dev/docs/documentation/guides/self-hosting"
|
||||
trailingIcon="external-link"
|
||||
trailingIconClassName="h-3 w-3 text-indigo-500 transition group-hover:text-indigo-400"
|
||||
>
|
||||
self-host
|
||||
</TextLink>{" "}
|
||||
Trigger.dev
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Form method="post" {...form.props}>
|
||||
<Fieldset>
|
||||
<InputGroup>
|
||||
<Input
|
||||
{...conform.input(code, { type: "text" })}
|
||||
placeholder="Your super secret invite code"
|
||||
icon="qr-code"
|
||||
autoFocus={Boolean(code.initialError)}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<FormError id={code.errorId}>{code.error}</FormError>
|
||||
</InputGroup>
|
||||
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"primary/small"}
|
||||
TrailingIcon={"arrow-right"}
|
||||
>
|
||||
Get access
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
</MainCenteredContainer>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export async function loader({ request }: LoaderArgs) {
|
||||
return redirectWithSuccessMessage(
|
||||
"/",
|
||||
request,
|
||||
"Please login to accept the invite.",
|
||||
"Please log in to accept the invite.",
|
||||
{ ephemeral: false }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { requestUrl } from "~/utils/requestUrl.server";
|
||||
export const meta: TypedMetaFunction<typeof loader, { root: RootLoader }> = ({
|
||||
parentsData,
|
||||
}) => ({
|
||||
title: `Login to Trigger.dev${appEnvTitleTag(parentsData.root.appEnv)}`,
|
||||
title: `Login to Trigger.dev${appEnvTitleTag(parentsData?.root.appEnv)}`,
|
||||
});
|
||||
|
||||
export async function loader({ request }: LoaderArgs) {
|
||||
@@ -71,7 +71,7 @@ export default function LoginPage() {
|
||||
<a href="https://trigger.dev">
|
||||
<LogoIcon className="mb-4 h-16 w-16" />
|
||||
</a>
|
||||
<FormTitle divide={false} title="Create your Trigger.dev account" />
|
||||
<FormTitle divide={false} title="Log in to Trigger.dev" />
|
||||
<Fieldset>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{data.showGithubAuth && (
|
||||
|
||||
@@ -34,7 +34,7 @@ import { appEnvTitleTag } from "~/utils";
|
||||
export const meta: TypedMetaFunction<typeof loader, { root: RootLoader }> = ({
|
||||
parentsData,
|
||||
}) => ({
|
||||
title: `Login to Trigger.dev${appEnvTitleTag(parentsData.root.appEnv)}`,
|
||||
title: `Login to Trigger.dev${appEnvTitleTag(parentsData?.root.appEnv)}`,
|
||||
});
|
||||
|
||||
export async function loader({ request }: LoaderArgs) {
|
||||
@@ -130,10 +130,7 @@ export default function LoginMagicLinkPage() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormTitle
|
||||
divide={false}
|
||||
title="Create your Trigger.dev account"
|
||||
/>
|
||||
<FormTitle divide={false} title="Log in to Trigger.dev" />
|
||||
<Fieldset className="flex w-full flex-col items-center gap-y-2">
|
||||
<InputGroup>
|
||||
<Label>Your email address</Label>
|
||||
|
||||
@@ -121,10 +121,6 @@ export class EndpointApi {
|
||||
|
||||
const anyBody = await response.json();
|
||||
|
||||
logger.debug("indexEndpoint() response from endpoint", {
|
||||
body: anyBody,
|
||||
});
|
||||
|
||||
const data = IndexEndpointResponseSchema.parse(anyBody);
|
||||
|
||||
return {
|
||||
@@ -177,6 +173,7 @@ export class EndpointApi {
|
||||
return {
|
||||
response,
|
||||
parser: RunJobResponseSchema,
|
||||
errorParser: ErrorWithStackSchema,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { $transaction, PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { workerQueue } from "../worker.server";
|
||||
import type { EndpointIndexSource } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { findEndpoint } from "~/models/endpoint.server";
|
||||
import { EndpointApi } from "../endpointApi.server";
|
||||
import { RegisterJobService } from "../jobs/registerJob.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { RegisterSourceService } from "../sources/registerSource.server";
|
||||
import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSchedule.server";
|
||||
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
|
||||
|
||||
export class IndexEndpointService {
|
||||
#prismaClient: PrismaClient;
|
||||
#registerJobService = new RegisterJobService();
|
||||
#registerSourceService = new RegisterSourceService();
|
||||
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
|
||||
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
@@ -17,14 +25,7 @@ export class IndexEndpointService {
|
||||
reason?: string,
|
||||
sourceData?: any
|
||||
) {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
const endpoint = await findEndpoint(id);
|
||||
|
||||
// Make a request to the endpoint to fetch a list of jobs
|
||||
const client = new EndpointApi(
|
||||
@@ -42,7 +43,19 @@ export class IndexEndpointService {
|
||||
const { jobs, sources, dynamicTriggers, dynamicSchedules } =
|
||||
indexResponse.data;
|
||||
|
||||
const queueName = `endpoint-${endpoint.id}`;
|
||||
logger.debug("Indexing endpoint", {
|
||||
endpointId: endpoint.id,
|
||||
endpointUrl: endpoint.url,
|
||||
endpointSlug: endpoint.slug,
|
||||
source: source,
|
||||
sourceData: sourceData,
|
||||
stats: {
|
||||
jobs: jobs.length,
|
||||
sources: sources.length,
|
||||
dynamicTriggers: dynamicTriggers.length,
|
||||
dynamicSchedules: dynamicSchedules.length,
|
||||
},
|
||||
});
|
||||
|
||||
const indexStats = {
|
||||
jobs: 0,
|
||||
@@ -51,94 +64,98 @@ export class IndexEndpointService {
|
||||
dynamicSchedules: 0,
|
||||
};
|
||||
|
||||
return await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
for (const job of jobs) {
|
||||
if (!job.enabled) {
|
||||
continue;
|
||||
}
|
||||
for (const job of jobs) {
|
||||
if (!job.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
indexStats.jobs++;
|
||||
try {
|
||||
await this.#registerJobService.call(endpoint, job);
|
||||
|
||||
await workerQueue.enqueue(
|
||||
"registerJob",
|
||||
{
|
||||
job,
|
||||
endpointId: endpoint.id,
|
||||
},
|
||||
{
|
||||
queueName,
|
||||
tx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
indexStats.sources++;
|
||||
|
||||
await workerQueue.enqueue(
|
||||
"registerSource",
|
||||
{
|
||||
source,
|
||||
endpointId: endpoint.id,
|
||||
},
|
||||
{
|
||||
queueName,
|
||||
tx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const dynamicTrigger of dynamicTriggers) {
|
||||
indexStats.dynamicTriggers++;
|
||||
|
||||
await workerQueue.enqueue(
|
||||
"registerDynamicTrigger",
|
||||
{
|
||||
dynamicTrigger,
|
||||
endpointId: endpoint.id,
|
||||
},
|
||||
{
|
||||
queueName,
|
||||
tx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const dynamicSchedule of dynamicSchedules) {
|
||||
indexStats.dynamicSchedules++;
|
||||
|
||||
await workerQueue.enqueue(
|
||||
"registerDynamicSchedule",
|
||||
{
|
||||
dynamicSchedule,
|
||||
endpointId: endpoint.id,
|
||||
},
|
||||
{
|
||||
queueName,
|
||||
tx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return await tx.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
stats: indexStats,
|
||||
data: {
|
||||
jobs,
|
||||
sources,
|
||||
dynamicTriggers,
|
||||
dynamicSchedules,
|
||||
},
|
||||
source,
|
||||
sourceData,
|
||||
reason,
|
||||
},
|
||||
indexStats.jobs++;
|
||||
} catch (error) {
|
||||
logger.debug("Failed to register job", {
|
||||
endpointId: endpoint.id,
|
||||
job,
|
||||
});
|
||||
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
await this.#registerSourceService.call(endpoint, source);
|
||||
|
||||
indexStats.sources++;
|
||||
} catch (error) {
|
||||
logger.debug("Failed to register source", {
|
||||
endpointId: endpoint.id,
|
||||
source,
|
||||
});
|
||||
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicTrigger of dynamicTriggers) {
|
||||
try {
|
||||
await this.#registerDynamicTriggerService.call(
|
||||
endpoint,
|
||||
dynamicTrigger
|
||||
);
|
||||
|
||||
indexStats.dynamicTriggers++;
|
||||
} catch (error) {
|
||||
logger.debug("Failed to register dynamic trigger", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicTrigger,
|
||||
});
|
||||
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dynamicSchedule of dynamicSchedules) {
|
||||
try {
|
||||
await this.#registerDynamicScheduleService.call(
|
||||
endpoint,
|
||||
dynamicSchedule
|
||||
);
|
||||
|
||||
indexStats.dynamicSchedules++;
|
||||
} catch (error) {
|
||||
logger.debug("Failed to register dynamic schedule", {
|
||||
endpointId: endpoint.id,
|
||||
dynamicSchedule,
|
||||
});
|
||||
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Endpoint indexing complete", {
|
||||
endpointId: endpoint.id,
|
||||
indexStats,
|
||||
source,
|
||||
sourceData,
|
||||
reason,
|
||||
});
|
||||
|
||||
return await this.#prismaClient.endpointIndex.create({
|
||||
data: {
|
||||
endpointId: endpoint.id,
|
||||
stats: indexStats,
|
||||
data: {
|
||||
jobs,
|
||||
sources,
|
||||
dynamicTriggers,
|
||||
dynamicSchedules,
|
||||
},
|
||||
source,
|
||||
sourceData,
|
||||
reason,
|
||||
},
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,10 @@ export const apisList = [
|
||||
identifier: "spotify",
|
||||
name: "Spotify",
|
||||
},
|
||||
{
|
||||
identifier: "stabilityai",
|
||||
name: "Stability AI",
|
||||
},
|
||||
{
|
||||
identifier: "stripe",
|
||||
name: "Stripe",
|
||||
@@ -204,6 +208,10 @@ export const apisList = [
|
||||
identifier: "twitter",
|
||||
name: "Twitter",
|
||||
},
|
||||
{
|
||||
identifier: "typeform",
|
||||
name: "Typeform",
|
||||
},
|
||||
{
|
||||
identifier: "whatsapp",
|
||||
name: "WhatsApp",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { github } from "./integrations/github";
|
||||
import { openai } from "./integrations/openai";
|
||||
import { plain } from "./integrations/plain";
|
||||
import { resend } from "./integrations/resend";
|
||||
import { slack } from "./integrations/slack";
|
||||
import { typeform } from "./integrations/typeform";
|
||||
import type { Integration } from "./types";
|
||||
|
||||
export class IntegrationCatalog {
|
||||
@@ -29,6 +31,8 @@ export const integrationCatalog = new IntegrationCatalog({
|
||||
// airtable,
|
||||
github,
|
||||
openai,
|
||||
plain,
|
||||
resend,
|
||||
slack,
|
||||
typeform,
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ const usageSample: HelpSample = {
|
||||
export const github: Integration = {
|
||||
identifier: "github",
|
||||
name: "GitHub",
|
||||
packageName: "@trigger.dev/github",
|
||||
packageName: "@trigger.dev/github@next",
|
||||
authenticationMethods: {
|
||||
oauth2: {
|
||||
name: "OAuth",
|
||||
|
||||
@@ -5,7 +5,7 @@ export const openai: Integration = {
|
||||
identifier: "openai",
|
||||
name: "OpenAI",
|
||||
description: "You can perform very long completions with the integration",
|
||||
packageName: "@trigger.dev/openai",
|
||||
packageName: "@trigger.dev/openai@latest",
|
||||
authenticationMethods: {
|
||||
apikey: {
|
||||
type: "apikey",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { Integration } from "../types";
|
||||
|
||||
export const plain: Integration = {
|
||||
identifier: "plain",
|
||||
name: "Plain",
|
||||
packageName: "@trigger.dev/plain@latest",
|
||||
authenticationMethods: {
|
||||
apikey: {
|
||||
type: "apikey",
|
||||
help: {
|
||||
samples: [
|
||||
{
|
||||
title: "Creating the client",
|
||||
code: `
|
||||
import { Plain } from "@trigger.dev/plain";
|
||||
|
||||
export const plain = new Plain({
|
||||
id: "__SLUG__",
|
||||
apiKey: process.env.PLAIN_API_KEY!,
|
||||
});
|
||||
`,
|
||||
},
|
||||
{
|
||||
title: "Using the client",
|
||||
code: `
|
||||
new Job(client, {
|
||||
id: "plain-playground",
|
||||
name: "Plain Playground",
|
||||
version: "0.1.1",
|
||||
integrations: {
|
||||
plain,
|
||||
},
|
||||
trigger: eventTrigger({
|
||||
name: "plain.playground",
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
const { customer } = await io.plain.upsertCustomer("upsert-customer", {
|
||||
identifier: {
|
||||
emailAddress: "rick.astley@gmail.com",
|
||||
},
|
||||
onCreate: {
|
||||
email: {
|
||||
email: "rick.astley@gmail.com",
|
||||
isVerified: true,
|
||||
},
|
||||
fullName: "Rick Astley",
|
||||
externalId: "u_123",
|
||||
},
|
||||
onUpdate: {
|
||||
fullName: {
|
||||
value: "Rick Astley",
|
||||
},
|
||||
externalId: {
|
||||
value: "u_123",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const foundCustomer = await io.plain.getCustomerById("get-customer", {
|
||||
customerId: customer.id,
|
||||
});
|
||||
|
||||
const timelineEntry = await io.plain.upsertCustomTimelineEntry(
|
||||
"upsert-timeline-entry",
|
||||
{
|
||||
customerId: customer.id,
|
||||
title: "My timeline entry",
|
||||
components: [
|
||||
{
|
||||
componentText: {
|
||||
text: \`This is a nice title\`,
|
||||
},
|
||||
},
|
||||
{
|
||||
componentDivider: {
|
||||
dividerSpacingSize: ComponentDividerSpacingSize.M,
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
textSize: ComponentTextSize.S,
|
||||
textColor: ComponentTextColor.Muted,
|
||||
text: "External id",
|
||||
},
|
||||
},
|
||||
{
|
||||
componentText: {
|
||||
text: foundCustomer?.externalId ?? "",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
`,
|
||||
highlight: [[12, 68]],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import type { Integration } from "../types";
|
||||
export const resend: Integration = {
|
||||
identifier: "resend",
|
||||
name: "Resend",
|
||||
packageName: "@trigger.dev/resend",
|
||||
packageName: "@trigger.dev/resend@next",
|
||||
authenticationMethods: {
|
||||
apikey: {
|
||||
type: "apikey",
|
||||
|
||||
@@ -45,7 +45,7 @@ new Job(client, {
|
||||
export const slack: Integration = {
|
||||
identifier: "slack",
|
||||
name: "Slack",
|
||||
packageName: "@trigger.dev/slack",
|
||||
packageName: "@trigger.dev/slack@next",
|
||||
authenticationMethods: {
|
||||
oauth2Bot: {
|
||||
name: "OAuth2 (Bot)",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Integration } from "../types";
|
||||
|
||||
export const typeform: Integration = {
|
||||
identifier: "typeform",
|
||||
name: "Typeform",
|
||||
description: "Use the Typeform API and trigger on new responses",
|
||||
packageName: "@trigger.dev/typeform@latest",
|
||||
authenticationMethods: {
|
||||
apikey: {
|
||||
type: "apikey",
|
||||
help: {
|
||||
samples: [
|
||||
{
|
||||
title: "Creating the client",
|
||||
code: `
|
||||
import { Typeform } from "@trigger.dev/typeform";
|
||||
|
||||
const typeform = new Typeform({
|
||||
id: "__SLUG__",
|
||||
apiKey: process.env.TYPEFORM_TOKEN!,
|
||||
});
|
||||
`,
|
||||
},
|
||||
{
|
||||
title: "Using the client",
|
||||
code: `
|
||||
new Job(client, {
|
||||
id: "typeform-response",
|
||||
name: "Typeform Response",
|
||||
version: "0.0.1",
|
||||
trigger: typeform.onFormResponse({
|
||||
uid: "<form uid>",
|
||||
tag: "<webhook tag>",
|
||||
}),
|
||||
integrations: {
|
||||
typeform,
|
||||
},
|
||||
run: async (payload, io, ctx) => {
|
||||
await io.typeform.getForm(payload.form_response.form_id);
|
||||
},
|
||||
});
|
||||
`,
|
||||
highlight: [
|
||||
[5, 8],
|
||||
[9, 11],
|
||||
[13, 13],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { env } from "~/env.server";
|
||||
import { findOrCreateUser } from "~/models/user.server";
|
||||
import type { AuthUser } from "./authUser";
|
||||
import { postAuthentication } from "./postAuth.server";
|
||||
import { logger } from "./logger.server";
|
||||
|
||||
export function addGitHubStrategy(
|
||||
authenticator: Authenticator<AuthUser>,
|
||||
@@ -24,6 +25,12 @@ export function addGitHubStrategy(
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("GitHub login", {
|
||||
emails,
|
||||
profile,
|
||||
extraParams,
|
||||
});
|
||||
|
||||
const { user, isNewUser } = await findOrCreateUser({
|
||||
email: emails[0].value,
|
||||
authenticationMethod: "GITHUB",
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { logger } from "../logger.server";
|
||||
import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { RegisterScheduleSourceService } from "../schedules/registerScheduleSource.server";
|
||||
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
|
||||
|
||||
export class RegisterJobService {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -26,22 +27,16 @@ export class RegisterJobService {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(endpointId: string, metadata: JobMetadata) {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
where: {
|
||||
id: endpointId,
|
||||
},
|
||||
include: {
|
||||
environment: {
|
||||
include: {
|
||||
project: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
public async call(
|
||||
endpointIdOrEndpoint: string | ExtendedEndpoint,
|
||||
metadata: JobMetadata
|
||||
) {
|
||||
const endpoint =
|
||||
typeof endpointIdOrEndpoint === "string"
|
||||
? await findEndpoint(endpointIdOrEndpoint)
|
||||
: endpointIdOrEndpoint;
|
||||
|
||||
await this.#upsertJob(endpoint, endpoint.environment, metadata);
|
||||
return this.#upsertJob(endpoint, endpoint.environment, metadata);
|
||||
}
|
||||
|
||||
async #upsertJob(
|
||||
@@ -49,12 +44,6 @@ export class RegisterJobService {
|
||||
environment: AuthenticatedEnvironment,
|
||||
metadata: JobMetadata
|
||||
): Promise<JobVersion> {
|
||||
logger.debug("Upserting job", {
|
||||
endpoint,
|
||||
organizationId: environment.organizationId,
|
||||
metadata,
|
||||
});
|
||||
|
||||
const integrations = new Map<string, Integration>();
|
||||
|
||||
for (const [, jobIntegration] of Object.entries(metadata.integrations)) {
|
||||
|
||||
@@ -5,6 +5,6 @@ import { sensitiveDataReplacer } from "./sensitiveDataReplacer";
|
||||
export const logger = new Logger(
|
||||
"webapp",
|
||||
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
|
||||
[],
|
||||
["examples"],
|
||||
sensitiveDataReplacer
|
||||
);
|
||||
|
||||
@@ -247,7 +247,7 @@ export class PerformRunExecutionService {
|
||||
run.event.sourceContext
|
||||
);
|
||||
|
||||
const { response, parser } = await client.executeJobRequest({
|
||||
const { response, parser, errorParser } = await client.executeJobRequest({
|
||||
event,
|
||||
job: {
|
||||
id: run.version.job.slug,
|
||||
@@ -288,13 +288,20 @@ export class PerformRunExecutionService {
|
||||
});
|
||||
}
|
||||
|
||||
const rawBody = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = safeJsonZodParse(errorParser, rawBody);
|
||||
|
||||
if (errorBody && errorBody.success) {
|
||||
return await this.#failRunExecutionWithRetry(execution, errorBody.data);
|
||||
}
|
||||
|
||||
return await this.#failRunExecutionWithRetry(execution, {
|
||||
message: `Endpoint responded with ${response.status} status code`,
|
||||
});
|
||||
}
|
||||
|
||||
const rawBody = await response.text();
|
||||
const safeBody = safeJsonZodParse(parser, rawBody);
|
||||
|
||||
if (!safeBody) {
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { SourceMetadata } from "@trigger.dev/internal";
|
||||
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import type { AuthenticatedEnvironment } from "../apiAuth.server";
|
||||
import { logger } from "../logger.server";
|
||||
import { workerQueue } from "../worker.server";
|
||||
import { generateSecret } from "./utils.server";
|
||||
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
|
||||
|
||||
export class RegisterSourceService {
|
||||
#prismaClient: PrismaClientOrTransaction;
|
||||
@@ -15,25 +15,16 @@ export class RegisterSourceService {
|
||||
}
|
||||
|
||||
public async call(
|
||||
endpointId: string,
|
||||
endpointIdOrEndpoint: string | ExtendedEndpoint,
|
||||
metadata: SourceMetadata,
|
||||
dynamicTriggerId?: string,
|
||||
accountId?: string,
|
||||
dynamicSource?: { id: string; metadata: any }
|
||||
) {
|
||||
const endpoint = await this.#prismaClient.endpoint.findUniqueOrThrow({
|
||||
where: {
|
||||
id: endpointId,
|
||||
},
|
||||
include: {
|
||||
environment: {
|
||||
include: {
|
||||
project: true,
|
||||
organization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const endpoint =
|
||||
typeof endpointIdOrEndpoint === "string"
|
||||
? await findEndpoint(endpointIdOrEndpoint)
|
||||
: endpointIdOrEndpoint;
|
||||
|
||||
return this.#upsertSource(
|
||||
endpoint,
|
||||
@@ -53,17 +44,21 @@ export class RegisterSourceService {
|
||||
accountId?: string,
|
||||
dynamicSource?: { id: string; metadata: any }
|
||||
) {
|
||||
logger.debug("Upserting source", {
|
||||
endpoint,
|
||||
organizationId: environment.organizationId,
|
||||
metadata,
|
||||
accountId,
|
||||
});
|
||||
|
||||
const key = [dynamicTriggerId, dynamicSource?.id, metadata.key]
|
||||
.filter(Boolean)
|
||||
.join(":");
|
||||
|
||||
const registrationJob = metadata.registerSourceJob
|
||||
? await this.#prismaClient.job.findUnique({
|
||||
where: {
|
||||
projectId_slug: {
|
||||
projectId: endpoint.projectId,
|
||||
slug: metadata.registerSourceJob.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const source = await $transaction(
|
||||
this.#prismaClient,
|
||||
async (tx) => {
|
||||
@@ -148,6 +143,18 @@ export class RegisterSourceService {
|
||||
},
|
||||
dynamicSourceId: dynamicSource?.id,
|
||||
dynamicSourceMetadata: dynamicSource?.metadata,
|
||||
sourceRegistrationJob:
|
||||
registrationJob && metadata.registerSourceJob
|
||||
? {
|
||||
connect: {
|
||||
jobId_version_environmentId: {
|
||||
jobId: registrationJob.id,
|
||||
version: metadata.registerSourceJob.version,
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
update: {
|
||||
endpoint: {
|
||||
@@ -158,6 +165,18 @@ export class RegisterSourceService {
|
||||
integration: { connect: { id: integration.id } },
|
||||
dynamicSourceId: dynamicSource?.id,
|
||||
dynamicSourceMetadata: dynamicSource?.metadata,
|
||||
sourceRegistrationJob:
|
||||
registrationJob && metadata.registerSourceJob
|
||||
? {
|
||||
connect: {
|
||||
jobId_version_environmentId: {
|
||||
jobId: registrationJob.id,
|
||||
version: metadata.registerSourceJob.version,
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
events: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
SCHEDULED_EVENT,
|
||||
} from "@trigger.dev/internal";
|
||||
import { PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
|
||||
|
||||
export class RegisterDynamicScheduleService {
|
||||
#prismaClient: PrismaClientOrTransaction;
|
||||
@@ -12,13 +13,18 @@ export class RegisterDynamicScheduleService {
|
||||
}
|
||||
|
||||
public async call(
|
||||
endpointId: string,
|
||||
endpointIdOrEndpoint: string | ExtendedEndpoint,
|
||||
metadata: RegisterDynamicSchedulePayload
|
||||
) {
|
||||
const endpoint =
|
||||
typeof endpointIdOrEndpoint === "string"
|
||||
? await findEndpoint(endpointIdOrEndpoint)
|
||||
: endpointIdOrEndpoint;
|
||||
|
||||
const dynamicTrigger = await this.#prismaClient.dynamicTrigger.upsert({
|
||||
where: {
|
||||
endpointId_slug_type: {
|
||||
endpointId: endpointId,
|
||||
endpointId: endpoint.id,
|
||||
slug: metadata.id,
|
||||
type: "SCHEDULE",
|
||||
},
|
||||
@@ -28,7 +34,7 @@ export class RegisterDynamicScheduleService {
|
||||
type: "SCHEDULE",
|
||||
endpoint: {
|
||||
connect: {
|
||||
id: endpointId,
|
||||
id: endpoint.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -47,7 +53,7 @@ export class RegisterDynamicScheduleService {
|
||||
},
|
||||
versions: {
|
||||
some: {
|
||||
endpointId,
|
||||
endpointId: endpoint.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DynamicTriggerEndpointMetadata } from "@trigger.dev/internal";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
|
||||
|
||||
export class RegisterDynamicTriggerService {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -10,13 +11,29 @@ export class RegisterDynamicTriggerService {
|
||||
}
|
||||
|
||||
public async call(
|
||||
endpointId: string,
|
||||
endpointIdOrEndpoint: string | ExtendedEndpoint,
|
||||
metadata: DynamicTriggerEndpointMetadata
|
||||
) {
|
||||
const endpoint =
|
||||
typeof endpointIdOrEndpoint === "string"
|
||||
? await findEndpoint(endpointIdOrEndpoint)
|
||||
: endpointIdOrEndpoint;
|
||||
|
||||
const registrationJob = metadata.registerSourceJob
|
||||
? await this.#prismaClient.job.findUnique({
|
||||
where: {
|
||||
projectId_slug: {
|
||||
projectId: endpoint.projectId,
|
||||
slug: metadata.registerSourceJob.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const dynamicTrigger = await this.#prismaClient.dynamicTrigger.upsert({
|
||||
where: {
|
||||
endpointId_slug_type: {
|
||||
endpointId: endpointId,
|
||||
endpointId: endpoint.id,
|
||||
slug: metadata.id,
|
||||
type: "EVENT",
|
||||
},
|
||||
@@ -26,11 +43,36 @@ export class RegisterDynamicTriggerService {
|
||||
type: "EVENT",
|
||||
endpoint: {
|
||||
connect: {
|
||||
id: endpointId,
|
||||
id: endpoint.id,
|
||||
},
|
||||
},
|
||||
sourceRegistrationJob:
|
||||
registrationJob && metadata.registerSourceJob
|
||||
? {
|
||||
connect: {
|
||||
jobId_version_environmentId: {
|
||||
jobId: registrationJob.id,
|
||||
version: metadata.registerSourceJob.version,
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
update: {
|
||||
sourceRegistrationJob:
|
||||
registrationJob && metadata.registerSourceJob
|
||||
? {
|
||||
connect: {
|
||||
jobId_version_environmentId: {
|
||||
jobId: registrationJob.id,
|
||||
version: metadata.registerSourceJob.version,
|
||||
environmentId: endpoint.environmentId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
update: {},
|
||||
include: {
|
||||
jobs: true,
|
||||
},
|
||||
@@ -44,7 +86,7 @@ export class RegisterDynamicTriggerService {
|
||||
},
|
||||
versions: {
|
||||
some: {
|
||||
endpointId,
|
||||
endpointId: endpoint.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user