Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 1dcb2ebcac | |||
| f65cb463cc | |||
| 2b4ce97f8a | |||
| 25abf319ab | |||
| 77807ab42a | |||
| f21990b14f | |||
| 1873d10e95 | |||
| 32ff3f3136 | |||
| 354b0aefb0 | |||
| aaa70a9aab | |||
| 3b90739087 | |||
| acaae993a5 | |||
| a74f0322e2 | |||
| 1a0286f6b0 | |||
| 64182483e2 | |||
| 87a25fba62 | |||
| 12803536fa | |||
| f160b34b34 | |||
| cca7da9da2 | |||
| 01cf5f3bdc | |||
| 33a928a9ea | |||
| c83443a41a | |||
| 374007bffd | |||
| 092382f319 | |||
| 79a0532e1d | |||
| 56c7c8b1ee | |||
| 722fe7b71e | |||
| 759793b2e3 | |||
| 99c6cd0371 | |||
| 9fbc5ab8de | |||
| 75ac2c9961 | |||
| 87330dd863 | |||
| 38fe9302cf | |||
| 33a455c593 | |||
| 37db0ac58d | |||
| 2cbf50b1cc | |||
| e26923eb19 | |||
| 3e63bcfb11 | |||
| 1e1c18c4a1 | |||
| 953e7fc918 | |||
| 8e147dbe0c | |||
| 86dbd5d1dc | |||
| e85ecfacba | |||
| f210532870 | |||
| 6c9ca686cb | |||
| 35b556d4a3 | |||
| c83339404a | |||
| 4172def1a3 | |||
| c99481ea8d | |||
| a11ddf6564 | |||
| 917a70fbe9 | |||
| 486d6818ba | |||
| 807b9d4c45 | |||
| 0013e78b1a | |||
| 9dd46c83fd | |||
| 1eb6bb8aa9 | |||
| 6c86946614 | |||
| 612b016a15 | |||
| be8beacaec | |||
| 4d316c5524 | |||
| 7fee5d6a87 | |||
| 0dd1aa5a5b | |||
| ed65ba4a73 | |||
| d39020370c | |||
| c18ba11819 | |||
| 5fbd7226eb | |||
| c9e27fd586 | |||
| 51b80eb09b | |||
| f409e9f329 | |||
| 304e6aaad5 | |||
| 3c5d02043e | |||
| 63980f63d4 | |||
| facd2b2131 | |||
| 25452588cc | |||
| 195d8c42fc | |||
| fe04b9d32d | |||
| d03b3aeb5f | |||
| 2ee33e70f7 | |||
| e9209629f7 | |||
| fa1e6bf5e6 | |||
| 93c3b10bf7 | |||
| 56cb2e3b07 | |||
| eabfb2457a |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
run context jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Better docs for io.try
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Added retrying and better messaging
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
IO Logging now respects the job and client logLevel, and only outputs locally when ioLogLocalEnabled is true
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
registerCron and unregisterCron jsdocs
|
||||
@@ -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
|
||||
---
|
||||
|
||||
io.runTask jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
[dev] Better handle errors when registering endpoint
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
io.sendEvent jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
deliverAt and timestamp event properties are now dates
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/openai": patch
|
||||
---
|
||||
|
||||
listModels returns the response.data instead of response.data.data
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
cronTrigger jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added JSDocs related to logging
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
intervalTrigger() jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Fixed responses from the PING action to match expected schema
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
jsdocs for DynamicTrigger and DynamicSchedule
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Export the client from the generated code and added code comments
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/github": patch
|
||||
---
|
||||
|
||||
Fixed some properties that had API urls instead of HTML urls
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/nextjs": patch
|
||||
---
|
||||
|
||||
Adaptors no longer take an options object
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/internal": patch
|
||||
---
|
||||
|
||||
backgroundFetch jsdocs
|
||||
+35
-2
@@ -15,9 +15,14 @@
|
||||
"@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"
|
||||
},
|
||||
"changesets": [
|
||||
"afraid-hornets-cheer",
|
||||
"beige-maps-hug",
|
||||
"big-tables-search",
|
||||
"bright-buses-join",
|
||||
"bright-horses-bathe",
|
||||
"chatty-cars-agree",
|
||||
"chilly-meals-warn",
|
||||
@@ -26,23 +31,51 @@
|
||||
"dirty-needles-kneel",
|
||||
"eight-buckets-agree",
|
||||
"eighty-queens-prove",
|
||||
"fast-ligers-wink",
|
||||
"four-ligers-perform",
|
||||
"friendly-parents-clean",
|
||||
"gentle-guests-leave",
|
||||
"great-meals-listen",
|
||||
"healthy-rockets-cry",
|
||||
"heavy-eyes-own",
|
||||
"heavy-mails-explain",
|
||||
"hot-eggs-protect",
|
||||
"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",
|
||||
"popular-cooks-boil",
|
||||
"quick-stingrays-own",
|
||||
"quiet-berries-tickle",
|
||||
"quiet-dryers-collect",
|
||||
"rich-clocks-attack",
|
||||
"shiny-mirrors-beam",
|
||||
"shy-bobcats-hear",
|
||||
"silent-mirrors-lay",
|
||||
"silver-insects-sneeze",
|
||||
"six-humans-happen",
|
||||
"sixty-squids-wink",
|
||||
"strange-jobs-shout",
|
||||
"strong-cars-visit",
|
||||
"stupid-files-cough",
|
||||
"sweet-cups-rescue",
|
||||
"sweet-snails-refuse",
|
||||
"tame-flowers-invite",
|
||||
"tasty-bats-flow",
|
||||
"ten-birds-confess",
|
||||
"tidy-planes-compete",
|
||||
"tough-walls-share",
|
||||
"wild-rats-mix"
|
||||
"wild-rats-mix",
|
||||
"young-birds-unite"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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/sdk": patch
|
||||
---
|
||||
|
||||
registerTrigger jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
io.registerInterval and io.unregisterInterval jsdocs
|
||||
@@ -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,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added JSdocs for io.wait and io.logger
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
isTriggerError jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
eventTrigger() jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
io.try jsdocs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added human readable cron expression property to cron triggers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added jsdocs for TriggerClient() and sendEvent()
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/slack": patch
|
||||
---
|
||||
|
||||
Added joinConversation task, and automatically try and join a public conversation if the bot is not in it when posting a message
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added JSdocs for Job
|
||||
@@ -106,41 +106,3 @@ jobs:
|
||||
env:
|
||||
REGISTRY: ghcr.io/triggerdotdev
|
||||
REPOSITORY: trigger.dev
|
||||
|
||||
- name: 🌐 Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::067435167850:role/GitHubAction-AssumeRoleWithAction
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: 🌐 Login to ECR
|
||||
id: ecr-login
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
|
||||
- name: 🌐 Push to Amazon ECR
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
- name: 🌐 Push 'latest' to Amazon ECR
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:latest
|
||||
docker push $REGISTRY/$REPOSITORY:latest
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
|
||||
- name: 🌐 Push commit hash to Amazon ECR
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
run: |
|
||||
docker tag release_build_image $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
env:
|
||||
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
|
||||
REPOSITORY: trigger.dev
|
||||
IMAGE_TAG: ${{ steps.get_version.outputs.version }}-${{ steps.get_commit.outputs.sha_short }}
|
||||
|
||||
@@ -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,
|
||||
@@ -13,8 +22,6 @@ import {
|
||||
} from "../primitives/ClientTabs";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import integrationButton from "./integration-button.png";
|
||||
import ngrok from "./ngrok.png";
|
||||
import publicUrl from "./public-url.png";
|
||||
import selectEnvironment from "./select-environment.png";
|
||||
import selectExample from "./select-example.png";
|
||||
|
||||
@@ -23,78 +30,11 @@ export function HowToSetupYourProject() {
|
||||
const appOrigin = useAppOrigin();
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Run your Next.js app" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Ensure your Next.js app is running locally on port 3000.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Create a tunnel to your Next.js app" />
|
||||
<StepNumber stepNumber="1" title="Run the CLI init command" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
There are a few ways to do this, but we recommend using{" "}
|
||||
<TextLink
|
||||
href="https://trigger.dev/docs/documentation/guides/tunneling-localhost"
|
||||
target="_blank"
|
||||
>
|
||||
ngrok
|
||||
</TextLink>
|
||||
. It’s free and easy to use and required to create a tunnel, making
|
||||
your local machine accessible to the internet.
|
||||
Run this CLI command in a terminal window from your Next.js project.
|
||||
</Paragraph>
|
||||
<StepNumber stepNumber="a" title="Install ngrok" className="mb-2" />
|
||||
<StepContentContainer>
|
||||
<ClientTabs defaultValue="mac">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"mac"}>Mac</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"windows"}>Windows</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"mac"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
fullWidth={false}
|
||||
value={`brew install ngrok/ngrok/ngrok`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"windows"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
fullWidth={false}
|
||||
value={`choco install ngrok`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="b" title="Open a new terminal window/tab" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>You need to leave this running.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="c" title="Create an http tunnel at port 3000" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
This creates a tunnel to your Next.js app.
|
||||
</Paragraph>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
fullWidth={false}
|
||||
className="mb-4"
|
||||
value={`ngrok http 3000`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber
|
||||
stepNumber="d"
|
||||
title={`Grab your "forwarding" URL from the ngrok output`}
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<img src={ngrok} className="mt-2 w-full" />
|
||||
</StepContentContainer>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Use the CLI" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
Copy this CLI command into a new terminal window.{" "}
|
||||
</Paragraph>
|
||||
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
@@ -105,42 +45,87 @@ export function HowToSetupYourProject() {
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure={`npx @trigger.dev/init@latest -k ••••••••• -t ${appOrigin} -u <ngrok_forwarding_url>`}
|
||||
value={`npx @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t ${appOrigin} -u <ngrok_forwarding_url>`}
|
||||
secure={`npx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`npx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure={`pnpm dlx @trigger.dev/init@latest -k ••••••••• -t ${appOrigin} -u <ngrok_forwarding_url>`}
|
||||
value={`pnpm dlx @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t ${appOrigin} -u <ngrok_forwarding_url>`}
|
||||
secure={`pnpm dlx @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`pnpm dlx @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
secure={`yarn @trigger.dev/init@latest -k ••••••••• -t ${appOrigin} -u <ngrok_forwarding_url>`}
|
||||
value={`yarn @trigger.dev/init@latest -k ${devEnvironment?.apiKey} -t ${appOrigin} -u <ngrok_forwarding_url>`}
|
||||
secure={`yarn @trigger.dev/cli@latest init -k ••••••••• -t ${appOrigin}`}
|
||||
value={`yarn @trigger.dev/cli@latest init -k ${devEnvironment?.apiKey} -t ${appOrigin}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
<Paragraph spacing>
|
||||
Use the "forwarding" URL from step 2d above to replace the -u
|
||||
placeholder text.
|
||||
<Paragraph spacing variant="small">
|
||||
It will ask you for a "unique ID for your endpoint". You can use the
|
||||
default by hitting enter.
|
||||
</Paragraph>
|
||||
<img src={publicUrl} className="mb-4 mt-2 w-full" />
|
||||
</StepContentContainer>
|
||||
|
||||
<StepNumber stepNumber="4" title="Run the CLI" />
|
||||
<StepNumber stepNumber="2" title="Run your Next.js app" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Ensure your Next.js app is running locally on port 3000.
|
||||
</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI dev command" />
|
||||
<StepContentContainer>
|
||||
<Paragraph spacing>
|
||||
It will add Trigger.dev to your existing Next.js project, setup a
|
||||
route and give you an example file.
|
||||
The CLI <InlineCode>dev</InlineCode> command allows the Trigger.dev
|
||||
service to send messages to your Next.js site. This is required for
|
||||
registering Jobs, triggering them and running Tasks. To achieve this
|
||||
it creates a tunnel (using{" "}
|
||||
<TextLink href="https://ngrok.com/">ngrok</TextLink>) so Trigger.dev
|
||||
can send messages to your machine.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
You should leave the <InlineCode>dev</InlineCode> command running when
|
||||
you're developing.
|
||||
</Paragraph>
|
||||
<Paragraph spacing>
|
||||
In a{" "}
|
||||
<strong className="text-bright">new terminal window or tab</strong>{" "}
|
||||
run:
|
||||
</Paragraph>
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`npx @trigger.dev/cli@latest dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`pnpm dlx @trigger.dev/cli@latest dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
className="mb-4"
|
||||
value={`yarn @trigger.dev/cli@latest dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="5" title="Check for Jobs" />
|
||||
<StepNumber stepNumber="4" title="Check for Jobs" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>
|
||||
Once you've run the CLI command, click Refresh to view your example
|
||||
@@ -159,6 +144,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 (
|
||||
<>
|
||||
@@ -190,7 +234,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>
|
||||
@@ -238,7 +282,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.
|
||||
@@ -247,13 +291,29 @@ export function HowToConnectAnIntegration() {
|
||||
);
|
||||
}
|
||||
|
||||
export function HowToUseThisIntegration() {
|
||||
export function HowToUseThisIntegration({
|
||||
integration,
|
||||
help,
|
||||
integrationClient,
|
||||
}: HelpPanelProps) {
|
||||
return (
|
||||
<>
|
||||
<StepNumber stepNumber="1" title="Step 1 title" />
|
||||
<StepNumber stepNumber="1" title="Install the package" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>Content</Paragraph>
|
||||
<HelpInstall packageName={integration.packageName} />
|
||||
</StepContentContainer>
|
||||
{help && (
|
||||
<>
|
||||
<StepNumber stepNumber="2" title="Create a Job" />
|
||||
<StepContentContainer>
|
||||
<HelpSamples
|
||||
help={help}
|
||||
integration={integration}
|
||||
integrationClient={integrationClient}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
@@ -1,26 +1,26 @@
|
||||
import {
|
||||
ApiAuthenticationMethodApiKey,
|
||||
Integration,
|
||||
} from "~/services/externalApis/types";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { Help, Integration } from "~/services/externalApis/types";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Header1 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsContent,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
} from "../primitives/ClientTabs";
|
||||
import { HelpInstall } from "./HelpInstall";
|
||||
import { HelpSamples, ReplacementData } from "./HelpSamples";
|
||||
|
||||
export type HelpPanelIntegration = Pick<
|
||||
Integration,
|
||||
"name" | "packageName" | "identifier"
|
||||
>;
|
||||
|
||||
export type HelpPanelProps = {
|
||||
integration: HelpPanelIntegration;
|
||||
help?: Help;
|
||||
integrationClient?: ReplacementData;
|
||||
};
|
||||
|
||||
export function ApiKeyHelp({
|
||||
integration,
|
||||
apiAuth,
|
||||
}: {
|
||||
integration: Integration;
|
||||
apiAuth: ApiAuthenticationMethodApiKey;
|
||||
}) {
|
||||
help,
|
||||
integrationClient,
|
||||
}: HelpPanelProps) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Header1 className="mb-2">
|
||||
@@ -34,44 +34,14 @@ export function ApiKeyHelp({
|
||||
First install the <InlineCode>{integration.packageName}</InlineCode>{" "}
|
||||
package using your preferred package manager. For example:
|
||||
</Paragraph>
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`npm install ${integration.packageName}@next`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`pnpm install ${integration.packageName}@next`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`yarn add ${integration.packageName}@next`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
{apiAuth.help.samples.map((sample, i) => (
|
||||
<div key={i}>
|
||||
<Paragraph spacing>{sample.title}</Paragraph>
|
||||
<CodeBlock
|
||||
code={sample.code}
|
||||
className="mb-4"
|
||||
highlightedRanges={sample.highlight}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<HelpInstall packageName={integration.packageName} />
|
||||
{help && (
|
||||
<HelpSamples
|
||||
help={help}
|
||||
integration={integration}
|
||||
integrationClient={integrationClient}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,12 +129,7 @@ function SelectedIntegrationMethod({
|
||||
case "apikey":
|
||||
const apiAuth = authMethods.find((a) => a.type === "apikey");
|
||||
if (!apiAuth) return null;
|
||||
return (
|
||||
<ApiKeyHelp
|
||||
integration={integration}
|
||||
apiAuth={apiAuth as ApiAuthenticationMethodApiKey}
|
||||
/>
|
||||
);
|
||||
return <ApiKeyHelp integration={integration} help={apiAuth.help} />;
|
||||
case "oauth2":
|
||||
return (
|
||||
<SelectOAuthMethod
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Integration } from "~/services/externalApis/types";
|
||||
import { InlineCode } from "../code/InlineCode";
|
||||
import {
|
||||
ClientTabs,
|
||||
ClientTabsList,
|
||||
ClientTabsTrigger,
|
||||
ClientTabsContent,
|
||||
} from "../primitives/ClientTabs";
|
||||
import { ClipboardField } from "../primitives/ClipboardField";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
|
||||
export function HelpInstall({ packageName }: { packageName: string }) {
|
||||
return (
|
||||
<>
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`npm install ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`pnpm install ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="secondary/medium"
|
||||
value={`yarn add ${packageName}`}
|
||||
className="mb-4"
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Integration } from "~/services/externalApis/types";
|
||||
import { HelpPanelIntegration, HelpPanelProps } from "./ApiKeyHelp";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
|
||||
export type ReplacementData = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export function HelpSamples({
|
||||
help,
|
||||
integrationClient,
|
||||
integration,
|
||||
}: HelpPanelProps) {
|
||||
return (
|
||||
<>
|
||||
{help &&
|
||||
help.samples.map((sample, i) => {
|
||||
const code = runReplacers(
|
||||
sample.code,
|
||||
integrationClient,
|
||||
integration
|
||||
);
|
||||
return (
|
||||
<div key={i}>
|
||||
<Paragraph spacing>{sample.title}</Paragraph>
|
||||
<CodeBlock
|
||||
code={code}
|
||||
className="mb-4"
|
||||
highlightedRanges={sample.highlight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const replacements = [
|
||||
{
|
||||
match: /__SLUG__/g,
|
||||
replacement: (
|
||||
data: ReplacementData | undefined,
|
||||
integration: HelpPanelIntegration
|
||||
) => {
|
||||
if (data) return data.slug;
|
||||
return integration.identifier;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function runReplacers(
|
||||
code: string,
|
||||
replacementData: ReplacementData | undefined,
|
||||
integration: HelpPanelIntegration
|
||||
) {
|
||||
replacements.forEach((r) => {
|
||||
code = code.replace(r.match, r.replacement(replacementData, integration));
|
||||
});
|
||||
|
||||
return code;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -126,10 +150,9 @@ export function JobsTable({
|
||||
</span>
|
||||
}
|
||||
value={
|
||||
<DateTime
|
||||
date={job.lastRun.createdAt}
|
||||
className={classForJobStatus(job.lastRun.status)}
|
||||
/>
|
||||
<span className={classForJobStatus(job.lastRun.status)}>
|
||||
<DateTime date={job.lastRun.createdAt} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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",
|
||||
icon: <BookOpenIcon className="h-5 w-5 shrink-0 text-blue-400" />,
|
||||
text: "text-blue-200",
|
||||
className: "border-blue-400/20 bg-blue-400/30",
|
||||
icon: <BookOpenIcon className="mt-0.5 h-5 w-5 shrink-0 text-blue-400" />,
|
||||
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,60 +1,114 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { useLocales } from "./LocaleProvider";
|
||||
|
||||
type DateTimeProps = {
|
||||
date: Date | string;
|
||||
timeZone?: string;
|
||||
className?: string;
|
||||
includeSeconds?: boolean;
|
||||
};
|
||||
|
||||
export const DateTime = ({ date, timeZone, className }: DateTimeProps) => {
|
||||
export const DateTime = ({
|
||||
date,
|
||||
timeZone = "UTC",
|
||||
includeSeconds = true,
|
||||
}: DateTimeProps) => {
|
||||
const locales = useLocales();
|
||||
|
||||
const realDate = typeof date === "string" ? new Date(date) : date;
|
||||
|
||||
const locales = useLocales();
|
||||
const isoString = realDate.toISOString();
|
||||
const formattedDate = new Intl.DateTimeFormat(locales, {
|
||||
const initialFormattedDateTime = formatDateTime(
|
||||
realDate,
|
||||
timeZone,
|
||||
locales,
|
||||
includeSeconds
|
||||
);
|
||||
|
||||
const [formattedDateTime, setFormattedDateTime] = useState<string>(
|
||||
initialFormattedDateTime
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
|
||||
|
||||
setFormattedDateTime(
|
||||
formatDateTime(
|
||||
realDate,
|
||||
resolvedOptions.timeZone,
|
||||
locales,
|
||||
includeSeconds
|
||||
)
|
||||
);
|
||||
}, [locales, includeSeconds, realDate]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
function formatDateTime(
|
||||
date: Date,
|
||||
timeZone: string,
|
||||
locales: string[],
|
||||
includeSeconds: boolean
|
||||
): string {
|
||||
return new Intl.DateTimeFormat(locales, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat(locales, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: includeSeconds ? "numeric" : undefined,
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export const DateTimeAccurate = ({ date, timeZone = "UTC" }: DateTimeProps) => {
|
||||
const locales = useLocales();
|
||||
|
||||
const realDate = typeof date === "string" ? new Date(date) : date;
|
||||
|
||||
const initialFormattedDateTime = formatDateTimeAccurate(
|
||||
realDate,
|
||||
timeZone,
|
||||
locales
|
||||
);
|
||||
|
||||
const [formattedDateTime, setFormattedDateTime] = useState<string>(
|
||||
initialFormattedDateTime
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
|
||||
|
||||
setFormattedDateTime(
|
||||
formatDateTimeAccurate(realDate, resolvedOptions.timeZone, locales)
|
||||
);
|
||||
}, [locales, realDate]);
|
||||
|
||||
return (
|
||||
<time dateTime={isoString} className={className}>
|
||||
{formattedDate} at {formattedTime}
|
||||
</time>
|
||||
<Fragment>
|
||||
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const DateTimeAccurate = ({ date, timeZone }: DateTimeProps) => {
|
||||
const realDate = typeof date === "string" ? new Date(date) : date;
|
||||
function formatDateTimeAccurate(
|
||||
date: Date,
|
||||
timeZone: string,
|
||||
locales: string[]
|
||||
): string {
|
||||
const milliseconds = `00${date.getMilliseconds()}`.slice(-3);
|
||||
|
||||
const locales = useLocales();
|
||||
const isoString = realDate.toISOString();
|
||||
const formattedDate = new Intl.DateTimeFormat(locales, {
|
||||
const formattedDateTime = new Intl.DateTimeFormat(locales, {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat(locales, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
timeZone,
|
||||
}).format(realDate);
|
||||
}).format(date);
|
||||
|
||||
const milliseconds = `00${realDate.getMilliseconds()}`.slice(-3);
|
||||
|
||||
return (
|
||||
<time dateTime={isoString}>
|
||||
{formattedDate} {formattedTime}.{milliseconds}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
return `${formatDateTime}.${milliseconds}`;
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,11 +120,7 @@ export function PageInfoProperty({
|
||||
{label}:
|
||||
</Paragraph>
|
||||
)}
|
||||
{typeof value === "string" || typeof value === "number" ? (
|
||||
<Paragraph variant="small">{value}</Paragraph>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
<Paragraph variant="small">{value}</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunList } from "~/presenters/RunListPresenter.server";
|
||||
import { formatDateTime, formatDuration } from "~/utils";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { runDashboardPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import {
|
||||
Table,
|
||||
@@ -56,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) => {
|
||||
@@ -75,9 +76,7 @@ export function RunsTable({
|
||||
<RunStatus status={run.status} />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{run.startedAt
|
||||
? formatDateTime(run.startedAt, "medium")
|
||||
: "–"}
|
||||
{run.startedAt ? <DateTime date={run.startedAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{formatDuration(run.startedAt, run.completedAt, {
|
||||
@@ -93,9 +92,7 @@ export function RunsTable({
|
||||
</TableCell>
|
||||
<TableCell to={path}>{run.version}</TableCell>
|
||||
<TableCell to={path}>
|
||||
{run.createdAt
|
||||
? formatDateTime(run.createdAt, "medium")
|
||||
: "–"}
|
||||
{run.createdAt ? <DateTime date={run.createdAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
</TableRow>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
import { PrismaClient, Prisma } from "@trigger.dev/database";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { logger } from "./services/logger.server";
|
||||
|
||||
export type PrismaTransactionClient = Omit<
|
||||
PrismaClient,
|
||||
@@ -15,15 +16,54 @@ function isTransactionClient(
|
||||
return !("$transaction" in prisma);
|
||||
}
|
||||
|
||||
export function $transaction<R>(
|
||||
function isPrismaKnownError(
|
||||
error: unknown
|
||||
): error is Prisma.PrismaClientKnownRequestError {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"code" in error &&
|
||||
typeof error.code === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export type PrismaTransactionOptions = {
|
||||
/** The maximum amount of time (in ms) Prisma Client will wait to acquire a transaction from the database. The default value is 2000ms. */
|
||||
maxWait?: number;
|
||||
|
||||
/** The maximum amount of time (in ms) the interactive transaction can run before being canceled and rolled back. The default value is 5000ms. */
|
||||
timeout?: number;
|
||||
|
||||
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
|
||||
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||
};
|
||||
|
||||
export async function $transaction<R>(
|
||||
prisma: PrismaClientOrTransaction,
|
||||
fn: (prisma: PrismaTransactionClient) => Promise<R>
|
||||
): Promise<R> {
|
||||
fn: (prisma: PrismaTransactionClient) => Promise<R>,
|
||||
options?: PrismaTransactionOptions
|
||||
): Promise<R | undefined> {
|
||||
if (isTransactionClient(prisma)) {
|
||||
return fn(prisma);
|
||||
}
|
||||
|
||||
return (prisma as PrismaClient).$transaction(fn);
|
||||
try {
|
||||
return await (prisma as PrismaClient).$transaction(fn, options);
|
||||
} catch (error) {
|
||||
if (isPrismaKnownError(error)) {
|
||||
logger.debug("prisma.$transaction error", {
|
||||
code: error.code,
|
||||
meta: error.meta,
|
||||
stack: error.stack,
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export { Prisma };
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { RemixBrowser, useLocation, useMatches } from "@remix-run/react";
|
||||
import { RemixBrowser } from "@remix-run/react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { useEffect } from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
|
||||
import { OperatingSystemContextProvider } from "./components/primitives/OperatingSystemProvider";
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import * as Worker from "~/services/worker.server";
|
||||
import { PassThrough } from "stream";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
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 type { EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { parseAcceptLanguage } from "intl-parse-accept-language";
|
||||
import isbot from "isbot";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import { PassThrough } from "stream";
|
||||
import * as Worker from "~/services/worker.server";
|
||||
import { LocaleContextProvider } from "./components/primitives/LocaleProvider";
|
||||
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(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { requestUrl } from "./utils";
|
||||
import { requestUrl } from "./utils/requestUrl.server";
|
||||
|
||||
export type TriggerFeatures = {
|
||||
isManagedCloud: boolean;
|
||||
@@ -10,7 +10,9 @@ export function featuresForRequest(request: Request): TriggerFeatures {
|
||||
const url = requestUrl(request);
|
||||
|
||||
const isManagedCloud =
|
||||
url.host === "cloud.trigger.dev" || process.env.NODE_ENV === "development";
|
||||
url.host === "cloud.trigger.dev" ||
|
||||
url.host === "test-cloud.trigger.dev" ||
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export function useOptionalIntegrationClient() {
|
||||
}
|
||||
|
||||
export function useIntegrationClient() {
|
||||
const client = useOptionalIntegrationClient();
|
||||
invariant(client, "Client must be defined");
|
||||
return client;
|
||||
const integration = useOptionalIntegrationClient();
|
||||
invariant(integration, "Integration must be defined");
|
||||
return integration;
|
||||
}
|
||||
|
||||
@@ -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] };
|
||||
}
|
||||
|
||||
@@ -132,11 +128,14 @@ export async function createEnvironment(
|
||||
) {
|
||||
const slug = envSlug(type);
|
||||
const apiKey = createApiKeyForEnv(type);
|
||||
const pkApiKey = createPkApiKeyForEnv(type);
|
||||
|
||||
return await prismaClient.runtimeEnvironment.create({
|
||||
data: {
|
||||
slug,
|
||||
apiKey,
|
||||
pkApiKey,
|
||||
autoEnableInternalSources: type !== "DEVELOPMENT",
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
@@ -157,6 +156,10 @@ function createApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
|
||||
return `tr_${envSlug(envType)}_${apiKeyId(12)}`;
|
||||
}
|
||||
|
||||
function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
|
||||
return `pk_${envSlug(envType)}_${apiKeyId(16)}`;
|
||||
}
|
||||
|
||||
function envSlug(environmentType: RuntimeEnvironment["type"]) {
|
||||
switch (environmentType) {
|
||||
case "DEVELOPMENT": {
|
||||
|
||||
@@ -128,3 +128,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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ import { PrismaClient, prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { OAuthClientSchema } from "~/services/externalApis/types";
|
||||
import { integrationCatalog } from "~/services/externalApis/integrationCatalog.server";
|
||||
import {
|
||||
Help,
|
||||
HelpSchema,
|
||||
OAuthClientSchema,
|
||||
} from "~/services/externalApis/types";
|
||||
import { getSecretStore } from "~/services/secrets/secretStore.server";
|
||||
|
||||
export class IntegrationClientPresenter {
|
||||
@@ -31,14 +36,18 @@ export class IntegrationClientPresenter {
|
||||
slug: true,
|
||||
authMethod: {
|
||||
select: {
|
||||
key: true,
|
||||
type: true,
|
||||
name: true,
|
||||
help: true,
|
||||
},
|
||||
},
|
||||
authSource: true,
|
||||
definition: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
packageName: true,
|
||||
},
|
||||
},
|
||||
connectionType: true,
|
||||
@@ -93,6 +102,10 @@ export class IntegrationClientPresenter {
|
||||
clientId = clientConfig?.id;
|
||||
}
|
||||
|
||||
const help = integration.authMethod?.help
|
||||
? HelpSchema.parse(integration.authMethod?.help)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: integration.id,
|
||||
title: integration.title ?? integration.slug,
|
||||
@@ -105,11 +118,13 @@ export class IntegrationClientPresenter {
|
||||
integration: {
|
||||
identifier: integration.definition.id,
|
||||
name: integration.definition.name,
|
||||
packageName: integration.definition.packageName,
|
||||
},
|
||||
authMethod: {
|
||||
type: integration.authMethod?.type ?? "local",
|
||||
name: integration.authMethod?.name ?? "Local Auth",
|
||||
},
|
||||
help,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 +1,9 @@
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { replacements } from "@trigger.dev/internal";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { Job } from "~/models/job.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server";
|
||||
import {
|
||||
ConnectionMetadataSchema,
|
||||
OAuthClientSchema,
|
||||
} from "~/services/externalApis/types";
|
||||
import { getSecretStore } from "~/services/secrets/secretStore.server";
|
||||
import { replacements } from "@trigger.dev/internal";
|
||||
|
||||
export class TestJobPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -84,6 +77,9 @@ export class TestJobPresenter {
|
||||
},
|
||||
},
|
||||
},
|
||||
project: {
|
||||
slug: projectSlug,
|
||||
},
|
||||
slug: jobSlug,
|
||||
},
|
||||
});
|
||||
|
||||
+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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+17
-3
@@ -21,6 +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, projectIntegrationsPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: {
|
||||
@@ -77,11 +78,24 @@ export default function Page() {
|
||||
{(open) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full gap-4",
|
||||
"grid h-fit gap-4",
|
||||
open ? "grid-cols-2" : "grid-cols-1"
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
@@ -117,7 +131,7 @@ export default function Page() {
|
||||
{project.jobs.length === 1 ? (
|
||||
<Callout
|
||||
variant="docs"
|
||||
href="https://trigger.dev/docs/documentation/quickstart#your-first-job"
|
||||
to={docsPath("documentation/quickstart#your-first-job")}
|
||||
className="my-3"
|
||||
>
|
||||
Create your first Job in code
|
||||
@@ -125,7 +139,7 @@ export default function Page() {
|
||||
) : (
|
||||
<Callout
|
||||
variant="docs"
|
||||
href="https://trigger.dev/docs/documentation/guides/create-a-job"
|
||||
to={docsPath("documentation/guides/create-a-job")}
|
||||
className="my-3"
|
||||
>
|
||||
Create another Job
|
||||
|
||||
+22
-21
@@ -1,5 +1,20 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { useEffect } from "react";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { Header1, Header2 } from "~/components/primitives/Headers";
|
||||
import { Hint } from "~/components/primitives/Hint";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
Sheet,
|
||||
SheetBody,
|
||||
@@ -7,25 +22,9 @@ import {
|
||||
SheetHeader,
|
||||
} from "~/components/primitives/Sheet";
|
||||
import { ClientEndpoint } from "~/presenters/EnvironmentsPresenter.server";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { useFetcher, useRevalidator } from "@remix-run/react";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Hint } from "~/components/primitives/Hint";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { formatDateTime } from "~/utils";
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { useEffect } from "react";
|
||||
import { endpointStreamingPath } from "~/utils/pathBuilder";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { bodySchema } from "../resources.environments.$environmentParam.endpoint";
|
||||
|
||||
type ConfigureEndpointSheetProps = {
|
||||
slug: string;
|
||||
@@ -147,9 +146,11 @@ export function ConfigureEndpointSheet({
|
||||
>
|
||||
<Paragraph variant="small" className="grow text-green-200">
|
||||
Endpoint configured. Last refreshed:{" "}
|
||||
{endpoint.latestIndex
|
||||
? formatDateTime(endpoint.latestIndex.updatedAt, "long")
|
||||
: "–"}
|
||||
{endpoint.latestIndex ? (
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
|
||||
+12
-9
@@ -1,13 +1,14 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { useMemo, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import {
|
||||
EnvironmentLabel,
|
||||
environmentTitle,
|
||||
} from "~/components/environments/EnvironmentLabel";
|
||||
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 {
|
||||
PageDescription,
|
||||
@@ -30,9 +31,9 @@ import {
|
||||
EnvironmentsPresenter,
|
||||
} from "~/presenters/EnvironmentsPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime, requestUrl } from "~/utils";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { ProjectParamSchema } from "~/utils/pathBuilder";
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
import { RuntimeEnvironmentType } from "../../../../../packages/database/src";
|
||||
import { ConfigureEndpointSheet } from "./ConfigureEndpointSheet";
|
||||
|
||||
@@ -118,7 +119,7 @@ export default function Page() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Header1 className="mb-2">Clients</Header1>
|
||||
<Header1 className="mb-2">Endpoints</Header1>
|
||||
<div className="flex flex-col gap-4">
|
||||
{clients.length > 0 ? (
|
||||
clients.map((client) => (
|
||||
@@ -128,9 +129,9 @@ export default function Page() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Environment</TableHeaderCell>
|
||||
<TableHeaderCell>Url</TableHeaderCell>
|
||||
<TableHeaderCell>Last refreshed</TableHeaderCell>
|
||||
<TableHeaderCell>Jobs</TableHeaderCell>
|
||||
<TableHeaderCell>Url</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -196,7 +197,7 @@ function EndpointRow({
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick} colSpan={4} alignment="right">
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<span className="text-rose-500">
|
||||
<span className="text-amber-500">
|
||||
The {environmentTitle({ type })} environment is not configured
|
||||
</span>
|
||||
<ButtonContent variant="primary/small">Configure</ButtonContent>
|
||||
@@ -212,15 +213,17 @@ function EndpointRow({
|
||||
<EnvironmentLabel environment={{ type }} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.url}</TableCell>
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex
|
||||
? formatDateTime(endpoint.latestIndex.updatedAt)
|
||||
: "–"}
|
||||
{endpoint.latestIndex ? (
|
||||
<DateTime date={endpoint.latestIndex.updatedAt} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>
|
||||
{endpoint.latestIndex?.stats.jobs ?? "–"}
|
||||
</TableCell>
|
||||
<TableCell onClick={onClick}>{endpoint.url}</TableCell>
|
||||
<TableCellChevron onClick={onClick} />
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
+7
-4
@@ -10,6 +10,7 @@ import { NoIntegrationSheet } from "~/components/integrations/NoIntegrationSheet
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
@@ -45,7 +46,6 @@ import {
|
||||
IntegrationsPresenter,
|
||||
} from "~/presenters/IntegrationsPresenter.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { formatDateTime } from "~/utils";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
ProjectParamSchema,
|
||||
@@ -351,7 +351,10 @@ function ConnectedIntegrationsList({
|
||||
: client.connectionsCount}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{formatDateTime(client.createdAt, "medium")}
|
||||
<DateTime
|
||||
date={client.createdAt}
|
||||
includeSeconds={false}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
</TableRow>
|
||||
@@ -387,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>
|
||||
@@ -448,7 +451,7 @@ function IntegrationsWithMissingFields({
|
||||
<IntegrationWithMissingFieldSheet
|
||||
integration={integration}
|
||||
organizationId={organizationId}
|
||||
button={formatDateTime(client.createdAt, "medium")}
|
||||
button={<DateTime date={client.createdAt} />}
|
||||
callbackUrl={callbackUrl}
|
||||
existingIntegration={client}
|
||||
className="flex w-full cursor-pointer justify-start"
|
||||
|
||||
+23
-2
@@ -1,8 +1,10 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { HowToUseThisIntegration } from "~/components/helpContent/HelpContentText";
|
||||
import { JobSkeleton } from "~/components/jobs/JobSkeleton";
|
||||
import { JobsTable } from "~/components/jobs/JobsTable";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
@@ -13,7 +15,11 @@ import { IntegrationClientJobsPresenter } from "~/presenters/IntegrationClientJo
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { IntegrationClientParamSchema } from "~/utils/pathBuilder";
|
||||
import {
|
||||
IntegrationClientParamSchema,
|
||||
docsIntegrationPath,
|
||||
docsRoot,
|
||||
} from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -90,7 +96,22 @@ export default function Page() {
|
||||
)}
|
||||
</div>
|
||||
<HelpContent title="How to use this Integration">
|
||||
<HowToUseThisIntegration />
|
||||
<HowToUseThisIntegration
|
||||
integration={{
|
||||
name: client.integration.name,
|
||||
identifier: client.integrationIdentifier,
|
||||
packageName: client.integration.packageName,
|
||||
}}
|
||||
integrationClient={client}
|
||||
help={client.help}
|
||||
/>
|
||||
<Callout
|
||||
variant="docs"
|
||||
to={docsIntegrationPath(client.integration.identifier)}
|
||||
>
|
||||
View the docs to learn more about using the{" "}
|
||||
{client.integration.name} Integration.
|
||||
</Callout>
|
||||
</HelpContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+8
-8
@@ -1,6 +1,7 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { connectionType } from "~/components/integrations/connectionType";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
Table,
|
||||
@@ -11,11 +12,8 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { useIntegrationClient } from "~/hooks/useIntegrationClient";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { IntegrationClientConnectionsPresenter } from "~/presenters/IntegrationClientConnectionsPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime } from "~/utils";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { IntegrationClientParamSchema } from "~/utils/pathBuilder";
|
||||
|
||||
@@ -43,8 +41,6 @@ export const handle: Handle = {
|
||||
|
||||
export default function Page() {
|
||||
const { connections } = useTypedLoaderData<typeof loader>();
|
||||
const client = useIntegrationClient();
|
||||
const project = useProject();
|
||||
|
||||
return (
|
||||
<Table>
|
||||
@@ -71,8 +67,12 @@ export default function Page() {
|
||||
<TableCell>
|
||||
<ExpiresAt expiresAt={connection.expiresAt} />
|
||||
</TableCell>
|
||||
<TableCell>{formatDateTime(connection.createdAt)}</TableCell>
|
||||
<TableCell>{formatDateTime(connection.updatedAt)}</TableCell>
|
||||
<TableCell>
|
||||
{<DateTime date={connection.createdAt} />}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{<DateTime date={connection.updatedAt} />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
@@ -98,7 +98,7 @@ function ExpiresAt({ expiresAt }: { expiresAt: Date | null }) {
|
||||
|
||||
return (
|
||||
<span className={inPast ? "text-rose-500" : ""}>
|
||||
{formatDateTime(expiresAt)}
|
||||
<DateTime date={expiresAt} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { connectionType } from "~/components/integrations/connectionType";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import {
|
||||
PageHeader,
|
||||
PageInfoGroup,
|
||||
@@ -17,7 +18,6 @@ import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { IntegrationClientPresenter } from "~/presenters/IntegrationClientPresenter.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { formatDateTime } from "~/utils";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
IntegrationClientParamSchema,
|
||||
@@ -57,6 +57,7 @@ export default function Integrations() {
|
||||
const { client } = useTypedLoaderData<typeof loader>();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
|
||||
let tabs = [
|
||||
{
|
||||
label: "Jobs",
|
||||
@@ -114,7 +115,7 @@ export default function Integrations() {
|
||||
<PageInfoProperty
|
||||
icon="calendar"
|
||||
label="Added"
|
||||
value={formatDateTime(client.createdAt)}
|
||||
value={<DateTime date={client.createdAt} />}
|
||||
/>
|
||||
</PageInfoGroup>
|
||||
</PageInfoRow>
|
||||
|
||||
+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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,8 +1,9 @@
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
|
||||
import { useRun } from "~/hooks/useRun";
|
||||
import { formatDateTime, formatDuration } from "~/utils";
|
||||
import { formatDuration } from "~/utils";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
@@ -28,14 +29,14 @@ export default function RunCompletedPage() {
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Started at"
|
||||
value={formatDateTime(run.startedAt, "long")}
|
||||
value={<DateTime date={run.startedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={formatDateTime(run.completedAt, "long")}
|
||||
value={<DateTime date={run.completedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.startedAt && run.completedAt && (
|
||||
|
||||
+15
-8
@@ -1,7 +1,9 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
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,
|
||||
@@ -12,8 +14,9 @@ import {
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
|
||||
import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime, formatDuration } from "~/utils";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { TaskParamsSchema } from "~/utils/pathBuilder";
|
||||
import {
|
||||
@@ -31,7 +34,6 @@ import {
|
||||
} from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/RunCard";
|
||||
import { TaskStatusIcon } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/TaskStatus";
|
||||
import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
|
||||
import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -56,6 +58,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
|
||||
export default function Page() {
|
||||
const { task } = useTypedLoaderData<typeof loader>();
|
||||
const locales = useLocales();
|
||||
|
||||
const {
|
||||
name,
|
||||
@@ -98,14 +101,14 @@ export default function Page() {
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Started at"
|
||||
value={formatDateTime(startedAt, "long")}
|
||||
value={<DateTime date={startedAt} />}
|
||||
/>
|
||||
)}
|
||||
{completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={formatDateTime(completedAt, "long")}
|
||||
value={<DateTime date={completedAt} />}
|
||||
/>
|
||||
)}
|
||||
{delayUntil && !completedAt && (
|
||||
@@ -113,7 +116,7 @@ export default function Page() {
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Continues at"
|
||||
value={formatDateTime(delayUntil, "long")}
|
||||
value={<DateTime date={delayUntil} />}
|
||||
/>
|
||||
<UpdatingDelay delayUntil={delayUntil} />
|
||||
</>
|
||||
@@ -160,9 +163,13 @@ export default function Page() {
|
||||
<TaskAttemptStatusLabel status={attempt.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{attempt.status === "PENDING" && attempt.runAt
|
||||
? formatDateTime(attempt.runAt, "long")
|
||||
: formatDateTime(attempt.updatedAt, "long")}
|
||||
<DateTime
|
||||
date={
|
||||
attempt.status === "PENDING" && attempt.runAt
|
||||
? attempt.runAt
|
||||
: attempt.updatedAt
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{attempt.error}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import { LoaderArgs } from "@remix-run/server-runtime";
|
||||
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 { useJob } from "~/hooks/useJob";
|
||||
import { useRun } from "~/hooks/useRun";
|
||||
import { EventDetailsPresenter } from "~/presenters/EventDetailsPresenter.server";
|
||||
import { formatDateTime } from "~/utils";
|
||||
import { RunParamsSchema } from "~/utils/pathBuilder";
|
||||
import {
|
||||
RunPanel,
|
||||
@@ -49,13 +49,13 @@ export default function Page() {
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Created"
|
||||
value={formatDateTime(timestamp, "long")}
|
||||
value={<DateTime date={timestamp} />}
|
||||
/>
|
||||
{deliveredAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={formatDateTime(deliveredAt, "long")}
|
||||
value={<DateTime date={deliveredAt} />}
|
||||
/>
|
||||
)}
|
||||
<RunPanelIconProperty icon="id" label="Event name" value={name} />
|
||||
|
||||
+1
-1
@@ -173,7 +173,7 @@ export function RunPanelIconProperty({
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
value: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
+19
-9
@@ -1,9 +1,9 @@
|
||||
import type { RuntimeEnvironmentType } from "@trigger.dev/internal";
|
||||
import { conform } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, Outlet, useNavigate, useRevalidator } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import type { RuntimeEnvironmentType } from "@trigger.dev/internal";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
@@ -13,6 +13,7 @@ import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import {
|
||||
@@ -42,12 +43,15 @@ import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import {
|
||||
redirectBackWithErrorMessage,
|
||||
redirectWithSuccessMessage,
|
||||
} from "~/models/message.server";
|
||||
import { RunPresenter } from "~/presenters/RunPresenter.server";
|
||||
import { ContinueRunService } from "~/services/runs/continueRun.server";
|
||||
import { ReRunService } from "~/services/runs/reRun.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime, formatDuration } from "~/utils";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
@@ -112,6 +116,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
const rerunService = new ReRunService();
|
||||
const run = await rerunService.call({ runId: runParam });
|
||||
|
||||
if (!run) {
|
||||
return redirectBackWithErrorMessage(request, "Unable to retry run");
|
||||
}
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
runDashboardPath(
|
||||
{ slug: organizationSlug },
|
||||
@@ -226,9 +234,11 @@ export default function Page() {
|
||||
icon={"calendar"}
|
||||
label={"Started"}
|
||||
value={
|
||||
run.startedAt
|
||||
? formatDateTime(run.startedAt)
|
||||
: "Not started yet"
|
||||
run.startedAt ? (
|
||||
<DateTime date={run.startedAt} />
|
||||
) : (
|
||||
"Not started yet"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
@@ -333,14 +343,14 @@ export default function Page() {
|
||||
<RunPanelIconProperty
|
||||
icon="calendar"
|
||||
label="Started at"
|
||||
value={formatDateTime(run.startedAt, "long")}
|
||||
value={<DateTime date={run.startedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.completedAt && (
|
||||
<RunPanelIconProperty
|
||||
icon="flag"
|
||||
label="Finished at"
|
||||
value={formatDateTime(run.completedAt, "long")}
|
||||
value={<DateTime date={run.completedAt} />}
|
||||
/>
|
||||
)}
|
||||
{run.startedAt && run.completedAt && (
|
||||
@@ -373,7 +383,7 @@ export default function Page() {
|
||||
) : (
|
||||
run.output === null && (
|
||||
<Paragraph variant="small">
|
||||
This run returned nothing
|
||||
This Run returned nothing.
|
||||
</Paragraph>
|
||||
)
|
||||
)}
|
||||
|
||||
+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"
|
||||
/>
|
||||
);
|
||||
|
||||
+15
-14
@@ -12,10 +12,7 @@ import { HowToRunATest } from "~/components/helpContent/HelpContentText";
|
||||
import { Button, ButtonContent } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { HelpTrigger } from "~/components/primitives/Help";
|
||||
import { HelpContent } from "~/components/primitives/Help";
|
||||
import { Help } from "~/components/primitives/Help";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help";
|
||||
import { Popover, PopoverContent } from "~/components/primitives/Popover";
|
||||
import {
|
||||
Select,
|
||||
@@ -25,18 +22,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import {
|
||||
redirectBackWithErrorMessage,
|
||||
redirectWithSuccessMessage,
|
||||
} from "~/models/message.server";
|
||||
import { TestJobPresenter } from "~/presenters/TestJobPresenter.server";
|
||||
import { TestJobService } from "~/services/jobs/testJob.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { formDataAsObject } from "~/utils/formData";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
JobParamsSchema,
|
||||
jobTestPath,
|
||||
runDashboardPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { JobParamsSchema, runDashboardPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -81,7 +76,6 @@ const schema = z.object({
|
||||
|
||||
//todo save the chosen environment to a cookie (for that user), use it to default the env dropdown
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam, jobParam } =
|
||||
JobParamsSchema.parse(params);
|
||||
|
||||
@@ -100,6 +94,13 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
versionId: submission.value.versionId,
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
return redirectBackWithErrorMessage(
|
||||
request,
|
||||
"Unable to start a test run: Something went wrong"
|
||||
);
|
||||
}
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
runDashboardPath(
|
||||
{ slug: organizationSlug },
|
||||
@@ -177,7 +178,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Help defaultOpen={!hasTestRuns}>
|
||||
<Help defaultOpen={true}>
|
||||
{(open) => (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -253,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) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { EnvelopeIcon, UserPlusIcon } from "@heroicons/react/20/solid";
|
||||
import { UserPlusIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import { useState } from "react";
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
LinkButton,
|
||||
} from "~/components/primitives/Buttons";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
@@ -39,16 +40,13 @@ import {
|
||||
} from "~/models/member.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDateTime, titleCase } from "~/utils";
|
||||
import {
|
||||
inviteTeamMemberPath,
|
||||
organizationTeamPath,
|
||||
resendInvitePath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
|
||||
import { resendSchema } from "../invite-resend";
|
||||
import { EnvelopeOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -169,7 +167,7 @@ export default function Page() {
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Header3>{invite.email}</Header3>
|
||||
<Paragraph variant="small">
|
||||
Invite sent {formatDateTime(invite.updatedAt, "medium")}
|
||||
Invite sent {<DateTime date={invite.updatedAt} />}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="flex grow items-center justify-end gap-4">
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user