diff --git a/apps/webapp/.eslintrc b/apps/webapp/.eslintrc index 187c257f8b..f292eef3cc 100644 --- a/apps/webapp/.eslintrc +++ b/apps/webapp/.eslintrc @@ -6,7 +6,7 @@ "files": ["*.ts", "*.tsx"], "rules": { // Autofixes imports from "@trigger.dev/core" to fine grained modules - "@trigger.dev/no-trigger-core-import": "error", + // "@trigger.dev/no-trigger-core-import": "error", // Normalize `import type {}` and `import { type }` "@typescript-eslint/consistent-type-imports": [ "warn", diff --git a/apps/webapp/app/api.server.ts b/apps/webapp/app/api.server.ts deleted file mode 100644 index b808913615..0000000000 --- a/apps/webapp/app/api.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiEventLog } from "@trigger.dev/core"; -import { EventRecord } from "@trigger.dev/database"; - -export function eventRecordToApiJson(eventRecord: EventRecord): ApiEventLog { - return { - id: eventRecord.eventId, - name: eventRecord.name, - payload: eventRecord.payload as any, - context: eventRecord.context as any, - timestamp: eventRecord.timestamp, - deliverAt: eventRecord.deliverAt, - deliveredAt: eventRecord.deliveredAt, - cancelledAt: eventRecord.cancelledAt, - }; -} diff --git a/apps/webapp/app/components/ActiveBadge.tsx b/apps/webapp/app/components/ActiveBadge.tsx deleted file mode 100644 index 0ad0c543ce..0000000000 --- a/apps/webapp/app/components/ActiveBadge.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { cn } from "~/utils/cn"; - -const variant = { - small: - "py-[0.25rem] px-1.5 text-xxs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-[0.125rem]", - normal: - "py-1 px-1.5 text-xs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-sm", -}; - -type ActiveBadgeProps = { - active: boolean; - className?: string; - badgeSize?: keyof typeof variant; -}; - -export function ActiveBadge({ active, className, badgeSize = "normal" }: ActiveBadgeProps) { - switch (active) { - case true: - return ( - - Active - - ); - case false: - return ( - - Disabled - - ); - } -} - -export function MissingIntegrationBadge({ - className, - badgeSize = "normal", -}: { - className?: string; - badgeSize?: keyof typeof variant; -}) { - return ( - - Missing Integration - - ); -} - -export function NewBadge({ - className, - badgeSize = "normal", -}: { - className?: string; - badgeSize?: keyof typeof variant; -}) { - return ( - - New! - - ); -} diff --git a/apps/webapp/app/components/BlankstateInstructions.tsx b/apps/webapp/app/components/BlankstateInstructions.tsx deleted file mode 100644 index 7388cf6d1a..0000000000 --- a/apps/webapp/app/components/BlankstateInstructions.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { cn } from "~/utils/cn"; -import { Header2 } from "./primitives/Headers"; - -export function BlankstateInstructions({ - children, - className, - title, -}: { - children: React.ReactNode; - className?: string; - title?: string; -}) { - return ( -
- {title && ( -
- {title} -
- )} - {children} -
- ); -} diff --git a/apps/webapp/app/components/ComingSoon.tsx b/apps/webapp/app/components/ComingSoon.tsx deleted file mode 100644 index 54d4f9e0a2..0000000000 --- a/apps/webapp/app/components/ComingSoon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { ReactNode } from "react"; -import { MainCenteredContainer } from "./layout/AppLayout"; -import { Header2 } from "./primitives/Headers"; -import { NamedIconInBox } from "./primitives/NamedIcon"; -import { Paragraph } from "./primitives/Paragraph"; - -type ComingSoonProps = { - title: string; - description: string; - icon: ReactNode; -}; - -export function ComingSoon({ title, description, icon }: ComingSoonProps) { - return ( - -
-
- {typeof icon === "string" ? ( - - ) : ( - icon - )} -
- Coming soon - {title} -
-
- - {description} - -
-
- ); -} diff --git a/apps/webapp/app/components/ErrorDisplay.tsx b/apps/webapp/app/components/ErrorDisplay.tsx index fcb720df88..c6544f6496 100644 --- a/apps/webapp/app/components/ErrorDisplay.tsx +++ b/apps/webapp/app/components/ErrorDisplay.tsx @@ -6,6 +6,7 @@ import { LinkButton } from "./primitives/Buttons"; import { Header1 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; import Spline from "@splinetool/react-spline"; +import { ReactNode } from "react"; type ErrorDisplayOptions = { button?: { @@ -38,7 +39,7 @@ export function RouteErrorDisplay(options?: ErrorDisplayOptions) { type DisplayOptionsProps = { title: string; - message?: string; + message?: ReactNode; } & ErrorDisplayOptions; export function ErrorDisplay({ title, message, button }: DisplayOptionsProps) { diff --git a/apps/webapp/app/components/HighlightInit.tsx b/apps/webapp/app/components/HighlightInit.tsx deleted file mode 100644 index fea26ef1ec..0000000000 --- a/apps/webapp/app/components/HighlightInit.tsx +++ /dev/null @@ -1,14 +0,0 @@ -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; -} diff --git a/apps/webapp/app/components/JobsStatusTable.tsx b/apps/webapp/app/components/JobsStatusTable.tsx deleted file mode 100644 index 88c0a8ff18..0000000000 --- a/apps/webapp/app/components/JobsStatusTable.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { RuntimeEnvironmentType } from "@trigger.dev/database"; -import { - Table, - TableBody, - TableCell, - TableHeader, - TableHeaderCell, - TableRow, -} from "~/components/primitives/Table"; -import { EnvironmentLabel } from "./environments/EnvironmentLabel"; -import { DateTime } from "./primitives/DateTime"; -import { ActiveBadge } from "./ActiveBadge"; - -export type JobEnvironment = { - type: RuntimeEnvironmentType; - lastRun?: Date; - version: string; - enabled: boolean; - concurrencyLimit?: number | null; - concurrencyLimitGroup?: { name: string; concurrencyLimit: number } | null; -}; - -type JobStatusTableProps = { - environments: JobEnvironment[]; - displayStyle?: "short" | "long"; -}; - -export function JobStatusTable({ environments, displayStyle = "short" }: JobStatusTableProps) { - return ( - - - - Env - Last Run - {displayStyle === "long" && Concurrency} - Version - Status - - - - {environments.map((environment, index) => ( - - - - - - {environment.lastRun ? : "Never Run"} - - {displayStyle === "long" && ( - - {environment.concurrencyLimitGroup ? ( - - {environment.concurrencyLimitGroup.name} - - ({environment.concurrencyLimitGroup.concurrencyLimit}) - - - ) : typeof environment.concurrencyLimit === "number" ? ( - {environment.concurrencyLimit} - ) : ( - Not specified - )} - - )} - - {environment.version} - - - - - ))} - -
- ); -} diff --git a/apps/webapp/app/components/ListPagination.tsx b/apps/webapp/app/components/ListPagination.tsx index bdefc32399..56a5c03380 100644 --- a/apps/webapp/app/components/ListPagination.tsx +++ b/apps/webapp/app/components/ListPagination.tsx @@ -1,6 +1,7 @@ +import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; import { useLocation } from "@remix-run/react"; +import { z } from "zod"; import { LinkButton } from "~/components/primitives/Buttons"; -import { Direction } from "~/components/runs/RunStatuses"; import { cn } from "~/utils/cn"; type List = { @@ -10,6 +11,9 @@ type List = { }; }; +export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]); +export type Direction = z.infer; + export function ListPagination({ list, className }: { list: List; className?: string }) { return (
@@ -26,7 +30,7 @@ function NextButton({ cursor }: { cursor?: string }) { - + Documentation
diff --git a/apps/webapp/app/components/NoMobileOverlay.tsx b/apps/webapp/app/components/NoMobileOverlay.tsx deleted file mode 100644 index 3a22850eeb..0000000000 --- a/apps/webapp/app/components/NoMobileOverlay.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { XMarkIcon, DevicePhoneMobileIcon } from "@heroicons/react/24/outline"; -import { Paragraph } from "./primitives/Paragraph"; -import { LinkButton } from "./primitives/Buttons"; - -export function NoMobileOverlay() { - return ( - <> -
-
-
- - - Trigger.dev is currently only available on desktop. - - Back Home - -
-
- - ); -} diff --git a/apps/webapp/app/components/SetupCommands.tsx b/apps/webapp/app/components/SetupCommands.tsx index dcfae4f88b..47e94c3ba3 100644 --- a/apps/webapp/app/components/SetupCommands.tsx +++ b/apps/webapp/app/components/SetupCommands.tsx @@ -36,127 +36,6 @@ function usePackageManager() { return context; } -export function InitCommand({ appOrigin, apiKey }: { appOrigin: string; apiKey: string }) { - return ( - - - npm - pnpm - yarn - - - - - - - - - - - - ); -} - -export function RunDevCommand({ extra }: { extra?: string }) { - return ( - - - npm - pnpm - yarn - - - - - - - - - - - - ); -} - -export function TriggerDevCommand({ extra }: { extra?: string }) { - return ( - - - npm - pnpm - yarn - - - - - - - - - - - - ); -} - -export function TriggerDevStep({ extra }: { extra?: string }) { - return ( - <> - - In a separate terminal window or tab run: - - - - If you’re not running on the default you can specify the port by adding{" "} - --port 3001 to the end. - - - You should leave the dev command running when - you're developing. - - - ); -} - const v3PackageTag = "latest"; function getApiUrlArg() { diff --git a/apps/webapp/app/components/billing/v2/FreePlanUsage.tsx b/apps/webapp/app/components/billing/FreePlanUsage.tsx similarity index 96% rename from apps/webapp/app/components/billing/v2/FreePlanUsage.tsx rename to apps/webapp/app/components/billing/FreePlanUsage.tsx index 7a830a4fe9..adc5ba3241 100644 --- a/apps/webapp/app/components/billing/v2/FreePlanUsage.tsx +++ b/apps/webapp/app/components/billing/FreePlanUsage.tsx @@ -1,6 +1,6 @@ import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { motion, useMotionValue, useTransform } from "framer-motion"; -import { Paragraph } from "../../primitives/Paragraph"; +import { Paragraph } from "../primitives/Paragraph"; import { Link } from "@remix-run/react"; import { cn } from "~/utils/cn"; diff --git a/apps/webapp/app/components/billing/v3/UpgradePrompt.tsx b/apps/webapp/app/components/billing/UpgradePrompt.tsx similarity index 92% rename from apps/webapp/app/components/billing/v3/UpgradePrompt.tsx rename to apps/webapp/app/components/billing/UpgradePrompt.tsx index e63e8b382b..e9b0fc1c97 100644 --- a/apps/webapp/app/components/billing/v3/UpgradePrompt.tsx +++ b/apps/webapp/app/components/billing/UpgradePrompt.tsx @@ -3,9 +3,9 @@ import tileBgPath from "~/assets/images/error-banner-tile@2x.png"; import { MatchedOrganization, useOrganization } from "~/hooks/useOrganizations"; import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; import { v3BillingPath } from "~/utils/pathBuilder"; -import { LinkButton } from "../../primitives/Buttons"; -import { Icon } from "../../primitives/Icon"; -import { Paragraph } from "../../primitives/Paragraph"; +import { LinkButton } from "../primitives/Buttons"; +import { Icon } from "../primitives/Icon"; +import { Paragraph } from "../primitives/Paragraph"; import { DateTime } from "~/components/primitives/DateTime"; export function UpgradePrompt() { diff --git a/apps/webapp/app/components/billing/v3/UsageBar.tsx b/apps/webapp/app/components/billing/UsageBar.tsx similarity index 97% rename from apps/webapp/app/components/billing/v3/UsageBar.tsx rename to apps/webapp/app/components/billing/UsageBar.tsx index 7d4e5db238..e570a029e2 100644 --- a/apps/webapp/app/components/billing/v3/UsageBar.tsx +++ b/apps/webapp/app/components/billing/UsageBar.tsx @@ -1,7 +1,7 @@ import { cn } from "~/utils/cn"; import { formatCurrency } from "~/utils/numberFormatter"; -import { Paragraph } from "../../primitives/Paragraph"; -import { SimpleTooltip } from "../../primitives/Tooltip"; +import { Paragraph } from "../primitives/Paragraph"; +import { SimpleTooltip } from "../primitives/Tooltip"; import { motion } from "framer-motion"; type UsageBarProps = { diff --git a/apps/webapp/app/components/billing/v2/ConcurrentRunsChart.tsx b/apps/webapp/app/components/billing/v2/ConcurrentRunsChart.tsx deleted file mode 100644 index 88efe200ae..0000000000 --- a/apps/webapp/app/components/billing/v2/ConcurrentRunsChart.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { - Label, - Line, - LineChart, - ReferenceLine, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { Paragraph } from "../../primitives/Paragraph"; - -const tooltipStyle = { - display: "flex", - alignItems: "center", - gap: "0.5rem", - borderRadius: "0.25rem", - border: "1px solid #1A2434", - backgroundColor: "#0B1018", - padding: "0.3rem 0.5rem", - fontSize: "0.75rem", - color: "#E2E8F0", -}; - -type DataItem = { date: string; maxConcurrentRuns: number }; - -const dateFormatter = new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", -}); - -export function ConcurrentRunsChart({ - concurrentRunsLimit, - data, - hasConcurrencyData, -}: { - concurrentRunsLimit?: number; - data: DataItem[]; - hasConcurrencyData: boolean; -}) { - return ( -
- {!hasConcurrencyData && ( - - No concurrent Runs to show - - )} - - - { - if (!item.date) return ""; - const date = new Date(item.date); - if (date.getDate() === 1) { - return dateFormatter.format(date); - } - return `${date.getDate()}`; - }} - className="text-xs" - > - - - { - const dateString = data.at(0)?.payload.date; - if (!dateString) { - return ""; - } - - return dateFormatter.format(new Date(dateString)); - }} - /> - {concurrentRunsLimit && ( - - - )} - - - -
- ); -} diff --git a/apps/webapp/app/components/billing/v2/DailyRunsChat.tsx b/apps/webapp/app/components/billing/v2/DailyRunsChat.tsx deleted file mode 100644 index ddc36785b8..0000000000 --- a/apps/webapp/app/components/billing/v2/DailyRunsChat.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Label, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; -import { Paragraph } from "../../primitives/Paragraph"; - -const tooltipStyle = { - display: "flex", - alignItems: "center", - gap: "0.5rem", - borderRadius: "0.25rem", - border: "1px solid #1A2434", - backgroundColor: "#0B1018", - padding: "0.3rem 0.5rem", - fontSize: "0.75rem", - color: "#E2E8F0", -}; - -type DataItem = { date: string; runs: number }; - -const dateFormatter = new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", -}); - -export function DailyRunsChart({ - data, - hasDailyRunsData, -}: { - data: DataItem[]; - hasDailyRunsData: boolean; -}) { - return ( -
- {!hasDailyRunsData && ( - - No daily Runs to show - - )} - - - { - if (!item.date) return ""; - const date = new Date(item.date); - if (date.getDate() === 1) { - return dateFormatter.format(date); - } - return `${date.getDate()}`; - }} - className="text-xs" - > - - - { - const dateString = data.at(0)?.payload.date; - if (!dateString) { - return ""; - } - - return dateFormatter.format(new Date(dateString)); - }} - /> - - - -
- ); -} diff --git a/apps/webapp/app/components/billing/v2/PricingCalculator.tsx b/apps/webapp/app/components/billing/v2/PricingCalculator.tsx deleted file mode 100644 index 7ec7bc8cdb..0000000000 --- a/apps/webapp/app/components/billing/v2/PricingCalculator.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import * as Slider from "@radix-ui/react-slider"; -import { Plans, estimate } from "@trigger.dev/platform/v2"; -import { useCallback, useState } from "react"; -import { DefinitionTip } from "../../DefinitionTooltip"; -import { Header2 } from "../../primitives/Headers"; -import { Paragraph } from "../../primitives/Paragraph"; -import { formatCurrency, formatNumberCompact } from "~/utils/numberFormatter"; -import { cn } from "~/utils/cn"; - -export function PricingCalculator({ plans }: { plans: Plans }) { - const [selectedConcurrencyIndex, setSelectedConcurrencyIndex] = useState(0); - const concurrentRunTiers = [ - { code: "free", upto: plans.free.concurrentRuns?.freeAllowance! }, - ...(plans.paid.concurrentRuns?.pricing?.tiers ?? []), - ]; - const [runs, setRuns] = useState(0); - const runBrackets = [ - ...(plans.paid.runs?.pricing?.brackets.map((b, index, arr) => ({ - unitCost: b.unitCost, - from: index === 0 ? 0 : arr[index - 1].upto! + 1, - upto: b.upto ?? arr[index - 1].upto! * 10, - })) ?? []), - ]; - - const result = estimate({ - usage: { runs, concurrent_runs: concurrentRunTiers[selectedConcurrencyIndex].upto - 1 }, - plans: [plans.free, plans.paid], - }); - - return ( -
- - - -
- ); -} - -function ConcurrentRunsSlider({ - options, - selectedIndex, - setSelectedIndex, - cost, -}: { - options: { - code: string; - upto: number; - }[]; - selectedIndex: number; - setSelectedIndex: (index: number) => void; - cost: number; -}) { - const selectedOption = options[selectedIndex]; - - return ( -
-
-
-
- - - Concurrent runs - - - Up to {selectedOption.upto} -
- setSelectedIndex(value[0])} - max={options.length - 1} - step={1} - > - - - - - -
- {options.map((tier, i) => { - return ( - - {tier.upto} - - ); - })} -
-
-
- = - - {formatCurrency(cost, true)} - -
-
-
-
- ); -} - -const runIncrements = 10_000; -function RunsSlider({ - brackets, - runs, - setRuns, - cost, -}: { - brackets: { - from: number; - upto: number; - unitCost: number; - }[]; - runs: number; - setRuns: (value: number) => void; - cost: number; -}) { - const [value, setValue] = useState(0); - - const updateRuns = useCallback((value: number) => { - setValue(value); - const r = calculateRuns(value / runIncrements, brackets); - setRuns(r); - }, []); - - return ( -
-
-
-
- - - Runs - - - {formatNumberCompact(runs)} -
- updateRuns(value[0])} - max={runIncrements} - step={1} - > - - - - - -
- {brackets.map((bracket, i, arr) => { - const percentagePerBracket = 1 / arr.length; - return ( - - ); - })} - -
-
-
- = - - {formatCurrency(cost, true)} - -
-
-
-
- ); -} - -function calculateRuns(percentage: number, brackets: { from: number; upto: number }[]) { - //first we find which bucket we're in - const buckets = brackets.length; - const bucket = Math.min(Math.floor(percentage * buckets), brackets.length - 1); - const percentagePerBucket = 1 / buckets; - - //relevant bracket - let bracket = brackets[bucket]; - const from = bracket.from; - const upto = bracket.upto; - - //how far as we into the bracket - const percentageIntoBracket = (percentage - bucket * percentagePerBucket) / percentagePerBucket; - - //calculate the runs - const runs = Math.floor(from + (upto - from) * percentageIntoBracket); - return runs; -} - -function GrandTotal({ cost }: { cost: number }) { - return ( -
- Total monthly estimate - {formatCurrency(cost, true)} -
- ); -} - -function SliderMarker({ - percentage, - alignment, - text, -}: { - percentage: number; - alignment: "left" | "center" | "right"; - text: string; -}) { - return ( -
-
- {text} -
-
- ); -} diff --git a/apps/webapp/app/components/billing/v2/PricingTiers.tsx b/apps/webapp/app/components/billing/v2/PricingTiers.tsx deleted file mode 100644 index 54e4aedd61..0000000000 --- a/apps/webapp/app/components/billing/v2/PricingTiers.tsx +++ /dev/null @@ -1,529 +0,0 @@ -import { useForm } from "@conform-to/react"; -import { parse } from "@conform-to/zod"; -import { CheckIcon, XMarkIcon } from "@heroicons/react/24/solid"; -import { Form, useActionData, useNavigation } from "@remix-run/react"; -import { ActiveSubscription, Plan, Plans, SetPlanBodySchema } from "@trigger.dev/platform/v2"; -import { useState } from "react"; -import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; -import { cn } from "~/utils/cn"; -import { formatNumberCompact } from "~/utils/numberFormatter"; -import { DefinitionTip } from "../../DefinitionTooltip"; -import { Feedback } from "../../Feedback"; -import { Button, LinkButton } from "../../primitives/Buttons"; -import SegmentedControl from "../../primitives/SegmentedControl"; -import { RunsVolumeDiscountTable } from "./RunsVolumeDiscountTable"; -import { Spinner } from "../../primitives/Spinner"; - -const pricingDefinitions = { - concurrentRuns: { - title: "Concurrent runs", - content: "The number of runs that can be executed at the same time.", - }, - jobRuns: { - title: "Job runs", - content: "A single execution of a job.", - }, - jobs: { - title: "Jobs", - content: "A durable function that can be executed on a schedule or in response to an event.", - }, - tasks: { - title: "Tasks", - content: "The individual building blocks of a job run.", - }, - events: { - title: "Events", - content: "Events trigger jobs to start running.", - }, - integrations: { - title: "Integrations", - content: "Easily subscribe to webhooks and perform actions using APIs.", - }, -}; - -export function PricingTiers({ - organizationSlug, - plans, - className, - showActionText = true, - freeButtonPath, -}: { - organizationSlug: string; - plans: Plans; - className?: string; - showActionText?: boolean; - freeButtonPath?: string; -}) { - const currentPlan = useCurrentPlan(); - //if they've canceled, we set the subscription to undefined so they can re-upgrade - let currentSubscription = currentPlan?.subscription; - if (currentPlan?.subscription?.canceledAt) { - currentSubscription = undefined; - } - - return ( -
- - - -
- ); -} - -export function TierFree({ - plan, - organizationSlug, - showActionText, - currentSubscription, - buttonPath, -}: { - plan: Plan; - organizationSlug: string; - showActionText: boolean; - currentSubscription?: ActiveSubscription; - buttonPath?: string; -}) { - const lastSubmission = useActionData(); - const [form] = useForm({ - id: "subscribe", - // TODO: type this - lastSubmission: lastSubmission as any, - onValidate({ formData }) { - return parse(formData, { schema: SetPlanBodySchema }); - }, - }); - - const navigation = useNavigation(); - const isLoading = - (navigation.state === "submitting" || navigation.state === "loading") && - navigation.formData?.get("type") === "free"; - - const isCurrentPlan = - currentSubscription?.isPaying === undefined || currentSubscription?.isPaying === false; - - let actionText = "Select plan"; - - if (showActionText) { - if (isCurrentPlan) { - actionText = "Current Plan"; - } else { - actionText = "Downgrade"; - } - } - - return ( - -
-
- - Up to {plan.concurrentRuns?.freeAllowance}{" "} - - {pricingDefinitions.concurrentRuns.title} - - - -
- {buttonPath ? ( - - {actionText} - - ) : ( - - )} -
-
    - - Up to {plan.runs?.freeAllowance ? formatNumberCompact(plan.runs.freeAllowance) : ""}{" "} - - {pricingDefinitions.jobRuns.title} - - - - Unlimited{" "} - - jobs - - - - Unlimited{" "} - - tasks - - - - Unlimited{" "} - - events - - - Unlimited team members - 24 hour log retention - Community support - Custom integrations - Role-based access control - SSO - On-prem option -
- - - ); -} - -export function TierPro({ - plan, - organizationSlug, - showActionText, - currentSubscription, -}: { - plan: Plan; - organizationSlug: string; - showActionText: boolean; - currentSubscription?: ActiveSubscription; -}) { - const lastSubmission = useActionData(); - const [form] = useForm({ - id: "subscribe", - // TODO: type this - lastSubmission: lastSubmission as any, - onValidate({ formData }) { - return parse(formData, { schema: SetPlanBodySchema }); - }, - }); - - const navigation = useNavigation(); - const isLoading = - (navigation.state === "submitting" || navigation.state === "loading") && - navigation.formData?.get("planCode") === plan.code; - - const currentConcurrencyTier = currentSubscription?.plan.concurrentRuns.pricing?.code; - const [concurrentBracketCode, setConcurrentBracketCode] = useState( - currentConcurrencyTier ?? plan.concurrentRuns?.pricing?.tiers[0].code - ); - - const concurrencyTiers = plan.concurrentRuns?.pricing?.tiers ?? []; - const selectedTier = concurrencyTiers.find((c) => c.code === concurrentBracketCode); - - const freeRunCount = plan.runs?.pricing?.brackets[0].upto ?? 0; - const mostExpensiveRunCost = plan.runs?.pricing?.brackets[1]?.unitCost ?? 0; - - const isCurrentPlan = currentConcurrencyTier === concurrentBracketCode; - - let actionText = "Select plan"; - - if (showActionText) { - if (isCurrentPlan) { - actionText = "Current Plan"; - } else { - const currentTierIndex = concurrencyTiers.findIndex((c) => c.code === currentConcurrencyTier); - const selectedTierIndex = concurrencyTiers.findIndex((c) => c.code === concurrentBracketCode); - actionText = currentTierIndex < selectedTierIndex ? "Upgrade" : "Downgrade"; - } - } - - return ( - -
-
- -
- - {pricingDefinitions.concurrentRuns.title} - -
- - - ({ label: `Up to ${c.upto}`, value: c.code }))} - fullWidth - value={concurrentBracketCode} - variant="primary" - onChange={(v) => setConcurrentBracketCode(v)} - /> -
- -
-
    - - Includes {freeRunCount ? formatNumberCompact(freeRunCount) : ""}{" "} - - {pricingDefinitions.jobRuns.title} - - , then{" "} - - } - > - {"<"} ${(mostExpensiveRunCost * 1000).toFixed(2)}/1K runs - - - - Unlimited{" "} - - jobs - - - - Unlimited{" "} - - tasks - - - - Unlimited{" "} - - events - - - Unlimited team members - 7 day log retention - Dedicated Slack support - Custom integrations - Role-based access control - SSO - On-prem option -
- - - ); -} - -export function TierEnterprise() { - return ( - -
- - Flexible{" "} - - {pricingDefinitions.concurrentRuns.title} - - -
- - Contact us - - } - defaultValue="enterprise" - /> -
-
    - - Flexible{" "} - - {pricingDefinitions.jobRuns.title} - - - - Unlimited{" "} - - jobs - - - - Unlimited{" "} - - tasks - - - - Unlimited{" "} - - events - - - Unlimited team members - 30 day log retention - Priority support - - Custom{" "} - - {pricingDefinitions.integrations.title} - - - Role-based access control - SSO - On-prem option -
- - ); -} - -function TierContainer({ - children, - isHighlighted, -}: { - children: React.ReactNode; - isHighlighted?: boolean; -}) { - return ( -
- {children} -
- ); -} - -function Header({ - title, - cost: flatCost, - isHighlighted, -}: { - title: string; - cost?: number; - isHighlighted?: boolean; -}) { - return ( -
-

- {title} -

- {flatCost === 0 || flatCost ? ( -

- ${flatCost} - /month -

- ) : ( -

Custom

- )} -
- ); -} - -function TierLimit({ children }: { children: React.ReactNode }) { - return ( -
-
-
- {children} -
-
- ); -} - -function FeatureItem({ checked, children }: { checked?: boolean; children: React.ReactNode }) { - return ( -
  • - {checked ? ( - - ) : ( - - )} -
    - {children} -
    -
  • - ); -} diff --git a/apps/webapp/app/components/billing/v2/RunsVolumeDiscountTable.tsx b/apps/webapp/app/components/billing/v2/RunsVolumeDiscountTable.tsx deleted file mode 100644 index d8215a409e..0000000000 --- a/apps/webapp/app/components/billing/v2/RunsVolumeDiscountTable.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { RunPriceBracket } from "@trigger.dev/platform/v2"; -import { Header2 } from "../../primitives/Headers"; -import { Paragraph } from "../../primitives/Paragraph"; -import { formatNumberCompact } from "~/utils/numberFormatter"; - -export function RunsVolumeDiscountTable({ - className, - hideHeader = false, - brackets, -}: { - className?: string; - hideHeader?: boolean; - brackets: RunPriceBracket[]; -}) { - const runsVolumeDiscountRow = - "flex justify-between whitespace-nowrap border-b gap-16 border-grid-bright last:pb-0 last:border-none py-2"; - - const bracketData = bracketInfo(brackets); - - return ( -
    - {hideHeader ? null : Runs volume discount} -
      - {bracketData.map((bracket, index) => ( -
    • - {bracket.range} - {bracket.costLabel} -
    • - ))} -
    -
    - ); -} - -function bracketInfo(brackets: RunPriceBracket[]) { - return brackets.map((bracket, index) => { - const { upto, unitCost } = bracket; - - if (index === 0) { - return { - range: `First ${formatNumberCompact(upto!)}/mo`, - costLabel: "Free", - }; - } - - const from = brackets[index - 1].upto; - const fromFormatted = formatNumberCompact(from!); - const toFormatted = upto ? formatNumberCompact(upto) : undefined; - - const costLabel = `$${(unitCost * 1000).toFixed(2)}/1,000`; - - if (!upto) { - return { - range: `${fromFormatted} +`, - costLabel, - }; - } - - return { - range: `${fromFormatted}–${toFormatted}`, - costLabel, - }; - }); -} diff --git a/apps/webapp/app/components/billing/v2/UsageBar.tsx b/apps/webapp/app/components/billing/v2/UsageBar.tsx deleted file mode 100644 index 1a86f70215..0000000000 --- a/apps/webapp/app/components/billing/v2/UsageBar.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { cn } from "~/utils/cn"; -import { formatNumberCompact } from "~/utils/numberFormatter"; -import { Paragraph } from "../../primitives/Paragraph"; -import { SimpleTooltip } from "../../primitives/Tooltip"; -import { motion } from "framer-motion"; - -type UsageBarProps = { - numberOfCurrentRuns: number; - billingLimit?: number; - tierRunLimit?: number; - projectedRuns: number; - subscribedToPaidTier?: boolean; -}; - -export function UsageBar({ - numberOfCurrentRuns, - billingLimit, - tierRunLimit, - projectedRuns, - subscribedToPaidTier = false, -}: UsageBarProps) { - const getLargestNumber = Math.max( - numberOfCurrentRuns, - tierRunLimit ?? -Infinity, - projectedRuns, - billingLimit ?? -Infinity - ); - //creates a maximum range for the progress bar, add 10% to the largest number so the bar doesn't reach the end - const maxRange = Math.round(getLargestNumber * 1.1); - const tierRunLimitPercentage = tierRunLimit ? Math.round((tierRunLimit / maxRange) * 100) : 0; - const projectedRunsPercentage = Math.round((projectedRuns / maxRange) * 100); - const billingLimitPercentage = - billingLimit !== undefined ? Math.round((billingLimit / maxRange) * 100) : 0; - const usagePercentage = Math.round((numberOfCurrentRuns / maxRange) * 100); - - //cap the usagePercentage to the freeRunLimitPercentage - const usageCappedToLimitPercentage = Math.min(usagePercentage, tierRunLimitPercentage); - - return ( -
    -
    - {billingLimit && ( - - - - )} - {tierRunLimit && ( - - - - )} - {projectedRuns !== 0 && ( - - - - )} - - - - -
    -
    - ); -} - -const positions = { - topRow1: "bottom-0 h-9", - topRow2: "bottom-0 h-14", - bottomRow1: "top-0 h-9 items-end", - bottomRow2: "top-0 h-14 items-end", -}; - -type LegendProps = { - text: string; - value: number | string; - percentage: number; - position: keyof typeof positions; - tooltipContent: string; -}; - -function Legend({ text, value, position, percentage, tooltipContent }: LegendProps) { - const flipLegendPositionValue = 80; - const flipLegendPosition = percentage > flipLegendPositionValue ? true : false; - return ( -
    - - {text} - {value} - - } - variant="dark" - side="top" - content={tooltipContent} - className="z-50 h-fit" - /> -
    - ); -} diff --git a/apps/webapp/app/components/environments/EndpointIndexStatus.tsx b/apps/webapp/app/components/environments/EndpointIndexStatus.tsx deleted file mode 100644 index 41c87db471..0000000000 --- a/apps/webapp/app/components/environments/EndpointIndexStatus.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/20/solid"; -import { EndpointIndexStatus } from "@trigger.dev/database"; -import { cn } from "~/utils/cn"; -import { Spinner } from "../primitives/Spinner"; - -export function EndpointIndexStatusIcon({ status }: { status: EndpointIndexStatus }) { - switch (status) { - case "PENDING": - return ; - case "STARTED": - return ; - case "SUCCESS": - return ( - - ); - case "FAILURE": - return ; - } -} - -export function EndpointIndexStatusLabel({ status }: { status: EndpointIndexStatus }) { - switch (status) { - case "PENDING": - return ( - - {endpointIndexStatusTitle(status)} - - ); - case "STARTED": - return ( - - {endpointIndexStatusTitle(status)} - - ); - case "SUCCESS": - return ( - - {endpointIndexStatusTitle(status)} - - ); - case "FAILURE": - return ( - - {endpointIndexStatusTitle(status)} - - ); - } -} - -export function endpointIndexStatusTitle(status: EndpointIndexStatus): string { - switch (status) { - case "PENDING": - return "Pending"; - case "STARTED": - return "Started"; - case "SUCCESS": - return "Success"; - case "FAILURE": - return "Failure"; - } -} - -export function endpointIndexStatusClassNameColor(status: EndpointIndexStatus): string { - switch (status) { - case "PENDING": - return "text-text-dimmed"; - case "STARTED": - return "text-blue-500"; - case "SUCCESS": - return "text-green-500"; - case "FAILURE": - return "text-rose-500"; - } -} diff --git a/apps/webapp/app/components/event/EventDetail.tsx b/apps/webapp/app/components/event/EventDetail.tsx deleted file mode 100644 index aedb500c94..0000000000 --- a/apps/webapp/app/components/event/EventDetail.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { CodeBlock } from "../code/CodeBlock"; -import { DateTime } from "../primitives/DateTime"; -import { Header3 } from "../primitives/Headers"; -import { - RunPanel, - RunPanelBody, - RunPanelDivider, - RunPanelIconProperty, - RunPanelIconSection, -} from "~/components/run/RunCard"; -import { Event } from "~/presenters/EventPresenter.server"; - -export function EventDetail({ event }: { event: Event }) { - const { id, name, payload, context, timestamp, deliveredAt } = event; - - return ( - - - - } - /> - {deliveredAt && ( - } - /> - )} - - - - -
    - Payload - - Context - -
    -
    -
    - ); -} diff --git a/apps/webapp/app/components/events/EventStatuses.tsx b/apps/webapp/app/components/events/EventStatuses.tsx deleted file mode 100644 index 016d773fb5..0000000000 --- a/apps/webapp/app/components/events/EventStatuses.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; -import { DirectionSchema, FilterableEnvironment } from "~/components/runs/RunStatuses"; - -export const EventListSearchSchema = z.object({ - cursor: z.string().optional(), - direction: DirectionSchema.optional(), - environment: FilterableEnvironment.optional(), - from: z - .string() - .transform((value) => parseInt(value)) - .optional(), - to: z - .string() - .transform((value) => parseInt(value)) - .optional(), -}); diff --git a/apps/webapp/app/components/events/EventsFilters.tsx b/apps/webapp/app/components/events/EventsFilters.tsx deleted file mode 100644 index c210119349..0000000000 --- a/apps/webapp/app/components/events/EventsFilters.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useNavigate } from "@remix-run/react"; -import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; -import { EnvironmentLabel } from "../environments/EnvironmentLabel"; -import { Paragraph } from "../primitives/Paragraph"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "../primitives/SimpleSelect"; -import { EventListSearchSchema } from "./EventStatuses"; -import { environmentKeys, FilterableEnvironment } from "~/components/runs/RunStatuses"; -import { TimeFrameFilter } from "../runs/TimeFrameFilter"; -import { useCallback } from "react"; -import { Button } from "../primitives/Buttons"; -import { TrashIcon } from "@heroicons/react/20/solid"; - -export function EventsFilters() { - const navigate = useNavigate(); - const location = useOptimisticLocation(); - const searchParams = new URLSearchParams(location.search); - const { environment, from, to } = EventListSearchSchema.parse( - Object.fromEntries(searchParams.entries()) - ); - - const handleFilterChange = useCallback((filterType: string, value: string | undefined) => { - if (value) { - searchParams.set(filterType, value); - } else { - searchParams.delete(filterType); - } - searchParams.delete("cursor"); - searchParams.delete("direction"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); - - const handleTimeFrameChange = useCallback((range: { from?: number; to?: number }) => { - if (range.from) { - searchParams.set("from", range.from.toString()); - } else { - searchParams.delete("from"); - } - - if (range.to) { - searchParams.set("to", range.to.toString()); - } else { - searchParams.delete("to"); - } - - searchParams.delete("cursor"); - searchParams.delete("direction"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); - - const handleEnvironmentChange = (value: FilterableEnvironment | "ALL") => { - handleFilterChange("environment", value === "ALL" ? undefined : value); - }; - - const clearFilters = useCallback(() => { - searchParams.delete("status"); - searchParams.delete("environment"); - searchParams.delete("from"); - searchParams.delete("to"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); - - return ( -
    - - - - - - -
    - ); -} diff --git a/apps/webapp/app/components/events/EventsTable.tsx b/apps/webapp/app/components/events/EventsTable.tsx deleted file mode 100644 index c80de1d96a..0000000000 --- a/apps/webapp/app/components/events/EventsTable.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { StopIcon } from "@heroicons/react/24/outline"; -import { CheckIcon } from "@heroicons/react/24/solid"; -import { RuntimeEnvironmentType, User } from "@trigger.dev/database"; -import { EnvironmentLabel } from "../environments/EnvironmentLabel"; -import { DateTime } from "../primitives/DateTime"; -import { Paragraph } from "../primitives/Paragraph"; -import { Spinner } from "../primitives/Spinner"; -import { - Table, - TableBlankRow, - TableBody, - TableCell, - TableCellChevron, - TableHeader, - TableHeaderCell, - TableRow, -} from "../primitives/Table"; - -type EventTableItem = { - id: string; - name: string | null; - environment: { - type: RuntimeEnvironmentType; - userId?: string; - userName?: string; - }; - createdAt: Date | null; - isTest: boolean; - deliverAt: Date | null; - deliveredAt: Date | null; - cancelledAt: Date | null; - runs: number; -}; - -type EventsTableProps = { - total: number; - hasFilters: boolean; - events: EventTableItem[]; - isLoading?: boolean; - eventsParentPath: string; - currentUser: User; -}; - -export function EventsTable({ - total, - hasFilters, - events, - isLoading = false, - eventsParentPath, - currentUser, -}: EventsTableProps) { - return ( - - - - Event - Env - Received Time - Delivery Time - Delivered - Canceled Time - Test - Runs - - Go to page - - - - - {total === 0 && !hasFilters ? ( - - - - ) : events.length === 0 ? ( - - - - ) : ( - events.map((event) => { - const path = `${eventsParentPath}/events/${event.id}`; - const usernameForEnv = - currentUser.id !== event.environment.userId ? event.environment.userName : undefined; - - return ( - - {typeof event.name === "string" ? event.name : "-"} - - - - - {event.createdAt ? : "–"} - - - {event.deliverAt ? : "–"} - - - {event.deliveredAt ? : "–"} - - - {event.cancelledAt ? : "–"} - - - {event.isTest ? ( - - ) : ( - - )} - - {event.runs} - - - ); - }) - )} - {isLoading && ( - - Loading… - - )} - -
    - ); -} - -function NoEvents({ title }: { title: string }) { - return ( -
    - {title} -
    - ); -} diff --git a/apps/webapp/app/components/frameworks/FrameworkComingSoon.tsx b/apps/webapp/app/components/frameworks/FrameworkComingSoon.tsx deleted file mode 100644 index 5dbc99fd6e..0000000000 --- a/apps/webapp/app/components/frameworks/FrameworkComingSoon.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Squares2X2Icon } from "@heroicons/react/20/solid"; -import { GitHubDarkIcon } from "@trigger.dev/companyicons"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { projectSetupPath } from "~/utils/pathBuilder"; -import { LinkButton } from "../primitives/Buttons"; -import { Header1 } from "../primitives/Headers"; -import { NamedIcon } from "../primitives/NamedIcon"; -import { Paragraph } from "../primitives/Paragraph"; -export type FrameworkComingSoonProps = { - frameworkName: string; - githubIssueUrl: string; - githubIssueNumber: number; - children: React.ReactNode; -}; - -export function FrameworkComingSoon({ - frameworkName, - githubIssueUrl, - githubIssueNumber, - children, -}: FrameworkComingSoonProps) { - const organization = useOrganization(); - const project = useProject(); - - return ( -
    -
    {children}
    -
    - {frameworkName} is coming soon! - - Choose a different framework - -
    - - We're working hard to bring support for {frameworkName} in Trigger.dev. Follow along with - the GitHub issue or contribute and help us bring it to Trigger.dev faster. - - - triggerdotdev/trigger.dev -

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

    -
    - - View on GitHub - -
    -
    -
    - ); -} diff --git a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx b/apps/webapp/app/components/frameworks/FrameworkSelector.tsx deleted file mode 100644 index b81c567e8d..0000000000 --- a/apps/webapp/app/components/frameworks/FrameworkSelector.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid"; -import { Link } from "@remix-run/react"; -import { AstroLogo } from "~/assets/logos/AstroLogo"; -import { ExpressLogo } from "~/assets/logos/ExpressLogo"; -import { FastifyLogo } from "~/assets/logos/FastifyLogo"; -import { NestjsLogo } from "~/assets/logos/NestjsLogo"; -import { NextjsLogo } from "~/assets/logos/NextjsLogo"; -import { NuxtLogo } from "~/assets/logos/NuxtLogo"; -import { RedwoodLogo } from "~/assets/logos/RedwoodLogo"; -import { RemixLogo } from "~/assets/logos/RemixLogo"; -import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { cn } from "~/utils/cn"; -import { - projectSetupAstroPath, - projectSetupExpressPath, - projectSetupFastifyPath, - projectSetupNestjsPath, - projectSetupNextjsPath, - projectSetupNuxtPath, - projectSetupRedwoodPath, - projectSetupRemixPath, - projectSetupSvelteKitPath, -} from "~/utils/pathBuilder"; -import { Feedback } from "../Feedback"; -import { Button } from "../primitives/Buttons"; -import { Header1 } from "../primitives/Headers"; - -export function FrameworkSelector() { - const organization = useOrganization(); - const project = useProject(); - - return ( -
    -
    - Choose a framework to get started… - - Request a framework - - } - defaultValue="feature" - /> -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - ); -} - -type FrameworkLinkProps = { - children: React.ReactNode; - to: string; - supported?: boolean; -}; - -function FrameworkLink({ children, to, supported = false }: FrameworkLinkProps) { - return ( - - {children} - - ); -} diff --git a/apps/webapp/app/components/helpContent/HelpContentText.tsx b/apps/webapp/app/components/helpContent/HelpContentText.tsx deleted file mode 100644 index e16e459699..0000000000 --- a/apps/webapp/app/components/helpContent/HelpContentText.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { Link } from "@remix-run/react"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { StepNumber } from "~/components/primitives/StepNumber"; -import { useJob } from "~/hooks/useJob"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { docsPath, jobTestPath } from "~/utils/pathBuilder"; -import { CodeBlock } from "../code/CodeBlock"; -import { InlineCode } from "../code/InlineCode"; -import { EnvironmentLabel } from "../environments/EnvironmentLabel"; -import { HelpPanelProps } from "../integrations/ApiKeyHelp"; -import { HelpInstall } from "../integrations/HelpInstall"; -import { HelpSamples } from "../integrations/HelpSamples"; -import { LinkButton } from "../primitives/Buttons"; -import { Callout, variantClasses } from "../primitives/Callout"; -import { Header2 } from "../primitives/Headers"; -import { TextLink } from "../primitives/TextLink"; -import integrationButton from "./integration-button.png"; -import selectEnvironment from "./select-environment.png"; -import selectExample from "./select-example.png"; -import { StepContentContainer } from "../StepContentContainer"; -import { TriggerDevCommand } from "../SetupCommands"; -import { IntegrationIcon } from "~/assets/icons/IntegrationIcon"; -import { BookOpenIcon } from "@heroicons/react/20/solid"; - -export function HowToRunYourJob() { - const organization = useOrganization(); - const project = useProject(); - const job = useJob(); - - return ( - <> - There are two ways to run your Job: - - - - - You can perform a Run with any payload you want, or use one of our examples, on the test - page. - - - Test - - - - - - - Performing a real run depends on the type of Trigger your Job is using. - - - - How to run a Job - - - - - Scheduled Triggers do not trigger Jobs in the DEV Environment. When - developing locally you should use the{" "} - - Test feature - {" "} - to trigger any scheduled Jobs. - - - - ); -} - -export function HowToConnectAnIntegration() { - return ( - <> - - - - APIs marked with a - - - - are Trigger.dev Integrations. These Integrations make connecting to the API easier by - offering OAuth or API key authentication. All APIs can also be used with fetch or an SDK. - - - - - - - Follow the instructions for your chosen connection method in the popover form. If no - Integration exists yet, you can request one by clicking the "I want an Integration" - button. - - - - - - Once you've connected your API, it will appear in the list of Integrations below. You can - view details and manage your connection by selecting it from the table. - - - - View the Integration docs page for more information on connecting an API using an - Integration or other method. - - - ); -} - -export function HowToUseThisIntegration({ integration, help, integrationClient }: HelpPanelProps) { - return ( - <> - - - - - {help && ( - <> - - - - - - )} - - ); -} - -export function HowToDisableAJob({ - id, - name, - version, -}: { - id: string; - name: string; - version: string; -}) { - return ( - <> - - To disable a job, you need to set the enabled property to{" "} - false. - - - Set enabled to false - - } - /> - - - - - Run the @trigger.dev/cli dev command - - } - /> - - - If you aren't already running the dev command, run it now. - - - - - ); -} - -export function HowToUseApiKeysAndEndpoints() { - return ( - <> - - Environments and Endpoints are used to connect your server to the Trigger.dev platform. - - Environments - - Each environment has API Keys associated with it. The Server API Key is used to authenticate - your Jobs with the Trigger.dev platform. - - - The Server API Key you use for your{" "} - - Client - {" "} - is how we know which environment to run your code against: - - - - Development - - - } - /> - - - The DEV environment should only be used for local development. - It’s where you can test your Jobs before deploying them to servers. - - - Scheduled Triggers do not trigger Jobs in the DEV Environment. When you’re working locally - you should use the Test feature to trigger any scheduled Jobs. - - - - Staging - - - } - /> - - - The STAGING environment is where your Jobs will run in a staging - environment, meant to mirror your production environment. - - - - Production - - - } - /> - - - The PROD environment is where your Jobs will run in production. - It’s where you can run your Jobs against real data. - - - Endpoints - - An Endpoint is a URL on your server that Trigger.dev can connect to. This URL is used to - register Jobs, start them and orchestrate runs and retries. - - - DEV has multiple endpoints associated with it – one for each team - member. This allows each team member to run their own Jobs, without interfering with each - other. - - - All other environments have just a single endpoint (with a single URL) associated with them. - - Deployment - - Deployment uses Environments and Endpoints to connect your Jobs to the Trigger.dev platform. - - - Read the deployment guide to learn more. - - - ); -} - -export function WhatAreHttpEndpoints() { - return ( - <> - - HTTP endpoints allow you to trigger your Jobs from any webhooks. They require a bit more - work than using Integrations{" "} - but allow you to connect to any API. - - Getting started - - You need to define the HTTP endpoint in your code. To do this you use{" "} - client.defineHttpEndpoint(). This will create an HTTP endpoint. - - - Then you can create a Trigger from this by calling .onRequest() on - the created HTTP endpoint. - - - Read the HTTP endpoints guide to learn more. - - - An example: cal.com - - { - //this helper function makes verifying most webhooks easy - return await verifyRequestSignature({ - request, - headerName: "X-Cal-Signature-256", - secret: process.env.CALDOTCOM_SECRET!, - algorithm: "sha256", - }); - }, -}); - -client.defineJob({ - id: "http-caldotcom", - name: "HTTP Cal.com", - version: "1.0.0", - enabled: true, - //create a Trigger from the HTTP endpoint above. The filter is optional. - trigger: caldotcom.onRequest({ filter: { body: { triggerEvent: ["BOOKING_CANCELLED"] } } }), - run: async (request, io, ctx) => { - //note that when using HTTP endpoints, the first parameter is the request - //you need to get the body, usually it will be json so you do: - const body = await request.json(); - await io.logger.info("Body", body); - }, -});`} - /> - - ); -} - -export function HowToConnectHttpEndpoint() { - return ( - <> - Setting up your webhook - - To start receiving data you need to enter the Endpoint URL and secret into the API service - you want to receive webhooks from. - - - Go to the relevant API dashboard} /> - - - For example, if you want to receive webhooks from Cal.com then you should login to your - Cal.com account and go to their Settings/Developer/Webhooks page. - - - - Copy the Webhook URL and Secret} /> - - - A unique Webhook URL is created for each environment (Dev, Staging, and Prod). Jobs will - only be triggered from the relevant environment. - - - Copy the relevant Endpoint URL and secret from the table opposite and paste it into the - correct place in the API dashboard you located in the previous step. - - - - Add the Secret to your Environment variables} /> - - - You should also add the Secret to the Environment variables in your code and where you're - deploying. Usually in Node this means adding it to the .env file. - - - Use the secret in the verify() function of HTTP Endpoint. This - ensures that someone can't just send a request to your Endpoint and trigger a Job. - Different APIs do this verification in different ways – a common way is to have a header - that has a hash of the payload and secret. Refer to the API's documentation for more - information. - - - - Triggering runs - - - - In your code, you should use the .onRequest() function in a Job - Trigger. You can filter so only data that matches your criteria triggers the Job. - - - - - - If you're using the Staging or Prod environment, you need to make sure your code is - deployed. Deploy like you normally would –{" "} - - read our deployment guide - - . - - - - - - Now you need to actually perform an action on that third-party service that triggers the - webhook you've subscribed to. For example, add a new meeting using Cal.com. - - - - - Read the HTTP endpoints guide to learn more. - - - ); -} diff --git a/apps/webapp/app/components/helpContent/integration-button.png b/apps/webapp/app/components/helpContent/integration-button.png deleted file mode 100644 index fc7e26672c..0000000000 Binary files a/apps/webapp/app/components/helpContent/integration-button.png and /dev/null differ diff --git a/apps/webapp/app/components/helpContent/select-environment.png b/apps/webapp/app/components/helpContent/select-environment.png deleted file mode 100644 index a1f6f7f167..0000000000 Binary files a/apps/webapp/app/components/helpContent/select-environment.png and /dev/null differ diff --git a/apps/webapp/app/components/helpContent/select-example.png b/apps/webapp/app/components/helpContent/select-example.png deleted file mode 100644 index 9230f0eabd..0000000000 Binary files a/apps/webapp/app/components/helpContent/select-example.png and /dev/null differ diff --git a/apps/webapp/app/components/integrations/ApiKeyHelp.tsx b/apps/webapp/app/components/integrations/ApiKeyHelp.tsx deleted file mode 100644 index dd0d851389..0000000000 --- a/apps/webapp/app/components/integrations/ApiKeyHelp.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Help, Integration } from "~/services/externalApis/types"; -import { InlineCode } from "../code/InlineCode"; -import { Header1 } from "../primitives/Headers"; -import { Paragraph } from "../primitives/Paragraph"; -import { HelpInstall } from "./HelpInstall"; -import { HelpSamples, ReplacementData } from "./HelpSamples"; - -export type HelpPanelIntegration = Pick; - -export type HelpPanelProps = { - integration: HelpPanelIntegration; - help?: Help; - integrationClient?: ReplacementData; -}; - -export function ApiKeyHelp({ integration, help, integrationClient }: HelpPanelProps) { - return ( -
    - How to use {integration.name} with API keys - - You can use API keys to authenticate with {integration.name}. Your API keys won't leave your - server, we'll never see them. - - - First install the {integration.packageName} package using your - preferred package manager. For example: - - - {help && ( - - )} -
    - ); -} diff --git a/apps/webapp/app/components/integrations/ConnectToIntegrationSheet.tsx b/apps/webapp/app/components/integrations/ConnectToIntegrationSheet.tsx deleted file mode 100644 index 37c80df5e6..0000000000 --- a/apps/webapp/app/components/integrations/ConnectToIntegrationSheet.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState } from "react"; -import { Integration } from "~/services/externalApis/types"; -import { apiReferencePath, docsIntegrationPath } from "~/utils/pathBuilder"; -import { LinkButton } from "../primitives/Buttons"; -import { Header1, Header2 } from "../primitives/Headers"; -import { NamedIconInBox } from "../primitives/NamedIcon"; -import { Paragraph } from "../primitives/Paragraph"; -import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton"; -import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet"; -import { ApiKeyHelp } from "./ApiKeyHelp"; -import { CustomHelp } from "./CustomHelp"; -import { SelectOAuthMethod } from "./SelectOAuthMethod"; - -type IntegrationMethod = "apikey" | "oauth2" | "custom"; - -export function ConnectToIntegrationSheet({ - integration, - organizationId, - button, - className, - callbackUrl, - icon, -}: { - integration: Integration; - organizationId: string; - button: React.ReactNode; - callbackUrl: string; - className?: string; - icon?: string; -}) { - const [integrationMethod, setIntegrationMethod] = useState( - undefined - ); - - const authMethods = Object.values(integration.authenticationMethods); - const hasApiKeyOption = authMethods.some((s) => s.type === "apikey"); - const hasOAuth2Option = authMethods.some((s) => s.type === "oauth2"); - - return ( - - {button} - - - -
    - {integration.name} - {integration.description && ( - {integration.description} - )} -
    - - View examples - - - View docs - -
    - - Choose an integration method - setIntegrationMethod(v as IntegrationMethod)} - > - {hasOAuth2Option && ( - - )} - {hasApiKeyOption && ( - - )} - - - {integrationMethod && ( - - )} - -
    -
    - ); -} - -function SelectedIntegrationMethod({ - integration, - organizationId, - method, - callbackUrl, -}: { - integration: Integration; - organizationId: string; - method: IntegrationMethod; - callbackUrl: string; -}) { - const authMethods = Object.values(integration.authenticationMethods); - - switch (method) { - case "apikey": - const apiAuth = authMethods.find((a) => a.type === "apikey"); - if (!apiAuth) return null; - return ; - case "oauth2": - return ( - - ); - } -} diff --git a/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx b/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx deleted file mode 100644 index 3f4e6159a8..0000000000 --- a/apps/webapp/app/components/integrations/ConnectToOAuthForm.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { conform, useForm } from "@conform-to/react"; -import { parse } from "@conform-to/zod"; -import { useFetcher, useLocation, useNavigation } from "@remix-run/react"; -import type { ConnectionType } from "@trigger.dev/database"; -import cuid from "cuid"; -import { useState } from "react"; -import simplur from "simplur"; -import { useFeatures } from "~/hooks/useFeatures"; -import { useTextFilter } from "~/hooks/useTextFilter"; -import { createSchema } from "~/routes/resources.connection.$organizationId.oauth2"; -import { ApiAuthenticationMethodOAuth2, Integration, Scope } from "~/services/externalApis/types"; -import { cn } from "~/utils/cn"; -import { CodeBlock } from "../code/CodeBlock"; -import { Button } from "../primitives/Buttons"; -import { CheckboxWithLabel } from "../primitives/Checkbox"; -import { Fieldset } from "../primitives/Fieldset"; -import { FormError } from "../primitives/FormError"; -import { Header2, Header3 } from "../primitives/Headers"; -import { Hint } from "../primitives/Hint"; -import { Input } from "../primitives/Input"; -import { InputGroup } from "../primitives/InputGroup"; -import { Label } from "../primitives/Label"; -import { Paragraph } from "../primitives/Paragraph"; - -export type Status = "loading" | "idle"; - -export function ConnectToOAuthForm({ - integration, - authMethod, - authMethodKey, - organizationId, - clientType, - callbackUrl, -}: { - integration: Integration; - authMethod: ApiAuthenticationMethodOAuth2; - authMethodKey: string; - organizationId: string; - clientType: ConnectionType; - callbackUrl: string; -}) { - const [id] = useState(cuid()); - const transition = useNavigation(); - const fetcher = useFetcher(); - const { isManagedCloud } = useFeatures(); - - const [form, { title, slug, scopes, hasCustomClient, customClientId, customClientSecret }] = - useForm({ - // TODO: type this - lastSubmission: fetcher.data as any, - shouldRevalidate: "onSubmit", - onValidate({ formData }) { - return parse(formData, { - // Create the schema without any constraint defined - schema: createSchema(), - }); - }, - }); - - const location = useLocation(); - - const [selectedScopes, setSelectedScopes] = useState>( - new Set(authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)) - ); - - const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud; - - const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp); - - const { filterText, setFilterText, filteredItems } = useTextFilter({ - items: authMethod.scopes, - filter: (scope, text) => { - if (scope.name.toLowerCase().includes(text.toLowerCase())) return true; - if (scope.description && scope.description.toLowerCase().includes(text.toLowerCase())) - return true; - - return false; - }, - }); - - return ( - -
    - - - - - - {form.error} - - - - - - This is used in your code to reference this connection. It must be unique for this - project. - - {slug.error} - - - - - {title.error} - - -
    - Use my OAuth App - - To use your own OAuth app, check the option below and insert the details. - - setUseMyOAuthApp(checked)} - {...conform.input(hasCustomClient, { type: "checkbox" })} - defaultChecked={requiresCustomOAuthApp} - /> - {useMyOAuthApp && ( -
    - - Set the callback url to - - -
    -
    - - - - - - - - -
    - {customClientId.error} -
    -
    - )} -
    - {authMethod.scopes.length > 0 && ( -
    - Scopes - - Select the scopes you want to grant to {integration.name} in order for it to access - your data. Note: If you try and perform an action in a Job that requires a scope you - haven’t granted, that task will fail. - - {/* - Select from popular scope collections - -
    - -
    */} -
    - Select {integration.name} scopes - - {simplur`${selectedScopes.size} scope[|s] selected`} - -
    - setFilterText(e.target.value)} - /> -
    - {filteredItems.length === 0 && ( - - No scopes match {filterText}. Try a different search query. - - )} - {authMethod.scopes.map((s) => { - return ( - a.label)} - description={s.description} - variant="description" - className={cn(filteredItems.find((f) => f.name === s.name) ? "" : "hidden")} - onChange={(isChecked) => { - if (isChecked) { - setSelectedScopes((selected) => { - selected.add(s.name); - return new Set(selected); - }); - } else { - setSelectedScopes((selected) => { - selected.delete(s.name); - return new Set(selected); - }); - } - }} - /> - ); - })} -
    -
    - )} -
    - -
    - {scopes.error} - -
    -
    - ); -} diff --git a/apps/webapp/app/components/integrations/CustomHelp.tsx b/apps/webapp/app/components/integrations/CustomHelp.tsx deleted file mode 100644 index d47602371a..0000000000 --- a/apps/webapp/app/components/integrations/CustomHelp.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from "react"; -import { CodeExample } from "~/routes/resources.codeexample"; -import { Api } from "~/services/externalApis/apis.server"; -import { cn } from "~/utils/cn"; -import { Feedback } from "../Feedback"; -import { Header1, Header2, Header3 } from "../primitives/Headers"; -import { Paragraph } from "../primitives/Paragraph"; -import { TextLink } from "../primitives/TextLink"; - -export function CustomHelp({ api }: { api: Api }) { - const [selectedExample, setSelectedExample] = useState(0); - - const changeCodeExample = (index: number) => { - setSelectedExample(index); - }; - - return ( -
    - Using {api.name} with an SDK or requests - - You can use Trigger.dev with any existing Node SDK or even just using fetch. You can - subscribe to any API with{" "} - - HTTP endpoints - {" "} - and perform actions by wrapping tasks using{" "} - - io.runTask - - . This makes your background job resumable and appear in our dashboard. - - - {api.examples && api.examples.length > 0 ? ( - <> - Example {api.name} code - - This is how you can use {api.name} with Trigger.dev. This code can be copied and - modified to suit your use-case. - - {api.examples.length > 1 && ( -
    - {api.examples?.map((example, index) => ( - - ))} -
    - )} - - - ) : ( - <> - Getting started with {api.name} - - We recommend searching for the official {api.name} Node SDK. If they have one, you can - install it and then use their API documentation to get started and create tasks. If they - don't, there are often third party SDKs you can use instead. - - - Please{" "} - - reach out to us - - } - defaultValue="help" - />{" "} - if you're having any issues connecting to {api.name}, we'll help you get set up as - quickly as possible. - - - )} -
    - ); -} diff --git a/apps/webapp/app/components/integrations/HelpInstall.tsx b/apps/webapp/app/components/integrations/HelpInstall.tsx deleted file mode 100644 index 468e98d108..0000000000 --- a/apps/webapp/app/components/integrations/HelpInstall.tsx +++ /dev/null @@ -1,45 +0,0 @@ -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 ( - <> - - - npm - pnpm - yarn - - - - - - - - - - - - - ); -} diff --git a/apps/webapp/app/components/integrations/HelpSamples.tsx b/apps/webapp/app/components/integrations/HelpSamples.tsx deleted file mode 100644 index 52a8d673c7..0000000000 --- a/apps/webapp/app/components/integrations/HelpSamples.tsx +++ /dev/null @@ -1,47 +0,0 @@ -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 ( -
    - {sample.title} - -
    - ); - })} - - ); -} - -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; -} diff --git a/apps/webapp/app/components/integrations/IntegrationWithMissingFieldSheet.tsx b/apps/webapp/app/components/integrations/IntegrationWithMissingFieldSheet.tsx deleted file mode 100644 index 44b1051041..0000000000 --- a/apps/webapp/app/components/integrations/IntegrationWithMissingFieldSheet.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { docsIntegrationPath } from "~/utils/pathBuilder"; -import { LinkButton } from "../primitives/Buttons"; -import { Header1 } from "../primitives/Headers"; -import { NamedIconInBox } from "../primitives/NamedIcon"; -import { Paragraph } from "../primitives/Paragraph"; -import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet"; -import { SelectOAuthMethod } from "./SelectOAuthMethod"; -import { Integration } from "~/services/externalApis/types"; -import { Client } from "~/presenters/IntegrationsPresenter.server"; - -export function IntegrationWithMissingFieldSheet({ - integration, - organizationId, - button, - callbackUrl, - existingIntegration, - className, -}: { - integration: Integration; - organizationId: string; - button: React.ReactNode; - callbackUrl: string; - existingIntegration: Client; - className?: string; -}) { - return ( - - {button} - - - -
    - {integration.name} - {integration.description && ( - {integration.description} - )} -
    - - View docs - -
    - - - -
    -
    - ); -} diff --git a/apps/webapp/app/components/integrations/NoIntegrationSheet.tsx b/apps/webapp/app/components/integrations/NoIntegrationSheet.tsx deleted file mode 100644 index 6998ffbaba..0000000000 --- a/apps/webapp/app/components/integrations/NoIntegrationSheet.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useFetcher } from "@remix-run/react"; -import React from "react"; -import { Api } from "~/services/externalApis/apis.server"; -import { Header1 } from "../primitives/Headers"; -import { NamedIconInBox } from "../primitives/NamedIcon"; -import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTrigger } from "../primitives/Sheet"; -import { CustomHelp } from "./CustomHelp"; - -export function NoIntegrationSheet({ - api, - requested, - button, -}: { - api: Api; - requested: boolean; - button: React.ReactNode; -}) { - const fetcher = useFetcher(); - const isLoading = fetcher.state !== "idle"; - - return ( - - {button} - - -
    - - {api.name} -
    -
    - - - -
    -
    - ); -} diff --git a/apps/webapp/app/components/integrations/SelectOAuthMethod.tsx b/apps/webapp/app/components/integrations/SelectOAuthMethod.tsx deleted file mode 100644 index 127031e51c..0000000000 --- a/apps/webapp/app/components/integrations/SelectOAuthMethod.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useState } from "react"; -import { ApiAuthenticationMethodOAuth2, Integration } from "~/services/externalApis/types"; -import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton"; -import type { ConnectionType } from "@trigger.dev/database"; -import { Header2 } from "../primitives/Headers"; -import { ConnectToOAuthForm } from "./ConnectToOAuthForm"; -import { Paragraph } from "../primitives/Paragraph"; -import { Client } from "~/presenters/IntegrationsPresenter.server"; -import { UpdateOAuthForm } from "./UpdateOAuthForm"; -import { LinkButton } from "../primitives/Buttons"; -import { BookOpenIcon } from "@heroicons/react/20/solid"; - -export function SelectOAuthMethod({ - integration, - organizationId, - callbackUrl, - existingIntegration, -}: { - existingIntegration?: Client; - integration: Integration; - organizationId: string; - callbackUrl: string; -}) { - const oAuthMethods = Object.entries(integration.authenticationMethods).filter( - (a): a is [string, ApiAuthenticationMethodOAuth2] => a[1].type === "oauth2" - ); - - const [oAuthKey, setOAuthKey] = useState( - oAuthMethods.length === 1 ? oAuthMethods[0][0] : undefined - ); - const [connectionType, setConnectionType] = useState(); - - const selectedOAuthMethod = oAuthKey - ? (integration.authenticationMethods[oAuthKey] as ApiAuthenticationMethodOAuth2) - : undefined; - - return ( - <> - {oAuthMethods.length > 1 && ( - <> - Select an OAuth option - setOAuthKey(v)} - > - {oAuthMethods.map(([key, auth]) => ( - - ))} - - - )} - {selectedOAuthMethod && ( - <> - Who is connecting to {integration.name} - setConnectionType(v as ConnectionType)} - > - - - - - )} - {selectedOAuthMethod && - connectionType && - oAuthKey && - (connectionType === "DEVELOPER" ? ( - existingIntegration ? ( - - ) : ( - - ) - ) : ( - <> - BYO Auth - - We support external authentication providers through Auth Resolvers. Read the docs to - learn more:{" "} - - Bring your own Auth - - - - ))} - - ); -} diff --git a/apps/webapp/app/components/integrations/UpdateOAuthForm.tsx b/apps/webapp/app/components/integrations/UpdateOAuthForm.tsx deleted file mode 100644 index f4e0f1fe2d..0000000000 --- a/apps/webapp/app/components/integrations/UpdateOAuthForm.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { conform, useForm } from "@conform-to/react"; -import { parse } from "@conform-to/zod"; -import { useFetcher, useLocation, useNavigation } from "@remix-run/react"; -import type { ConnectionType } from "@trigger.dev/database"; -import { useState } from "react"; -import simplur from "simplur"; -import { useFeatures } from "~/hooks/useFeatures"; -import { useTextFilter } from "~/hooks/useTextFilter"; -import { ApiAuthenticationMethodOAuth2, Integration, Scope } from "~/services/externalApis/types"; -import { cn } from "~/utils/cn"; -import { CodeBlock } from "../code/CodeBlock"; -import { Button } from "../primitives/Buttons"; -import { CheckboxWithLabel } from "../primitives/Checkbox"; -import { Fieldset } from "../primitives/Fieldset"; -import { FormError } from "../primitives/FormError"; -import { Header2, Header3 } from "../primitives/Headers"; -import { Input } from "../primitives/Input"; -import { InputGroup } from "../primitives/InputGroup"; -import { Label } from "../primitives/Label"; -import { Paragraph } from "../primitives/Paragraph"; -import { Client } from "~/presenters/IntegrationsPresenter.server"; -import { schema } from "~/routes/resources.connection.$organizationId.oauth2.$integrationId"; - -export type Status = "loading" | "idle"; - -export function UpdateOAuthForm({ - existingIntegration, - integration, - authMethod, - authMethodKey, - organizationId, - clientType, - callbackUrl, -}: { - existingIntegration: Client; - integration: Integration; - authMethod: ApiAuthenticationMethodOAuth2; - authMethodKey: string; - organizationId: string; - clientType: ConnectionType; - callbackUrl: string; -}) { - const transition = useNavigation(); - const fetcher = useFetcher(); - const { isManagedCloud } = useFeatures(); - - const [form, { title, scopes, hasCustomClient, customClientId, customClientSecret }] = useForm({ - // TODO: type this - lastSubmission: fetcher.data as any, - onValidate({ formData }) { - return parse(formData, { - schema, - }); - }, - }); - - const location = useLocation(); - - const [selectedScopes, setSelectedScopes] = useState>( - new Set(authMethod.scopes.filter((s) => s.defaultChecked).map((s) => s.name)) - ); - - const requiresCustomOAuthApp = clientType === "EXTERNAL" || !isManagedCloud; - - const [useMyOAuthApp, setUseMyOAuthApp] = useState(requiresCustomOAuthApp); - - const { filterText, setFilterText, filteredItems } = useTextFilter({ - items: authMethod.scopes, - filter: (scope, text) => { - if (scope.name.toLowerCase().includes(text.toLowerCase())) return true; - if (scope.description && scope.description.toLowerCase().includes(text.toLowerCase())) - return true; - - return false; - }, - }); - - return ( - -
    - - - - - - {form.error} - - - - - {existingIntegration.slug} - - - - - - {title.error} - - -
    - Use my OAuth App - - To use your own OAuth app, check the option below and insert the details. - - setUseMyOAuthApp(checked)} - {...conform.input(hasCustomClient, { type: "checkbox" })} - defaultChecked={requiresCustomOAuthApp} - /> - {useMyOAuthApp && ( -
    - - Set the callback url to - -
    -
    - - - - - - - - -
    - {customClientId.error} -
    -
    - )} -
    - {authMethod.scopes.length > 0 && ( -
    - Scopes - - Select the scopes you want to grant to {integration.name} in order for it to access - your data. Note: If you try and perform an action in a Job that requires a scope you - haven’t granted, that task will fail. - - {/* - Select from popular scope collections - -
    - -
    */} -
    - Select {integration.name} scopes - - {simplur`${selectedScopes.size} scope[|s] selected`} - -
    - setFilterText(e.target.value)} - /> -
    - {filteredItems.length === 0 && ( - - No scopes match {filterText}. Try a different search query. - - )} - {authMethod.scopes.map((s) => { - return ( - a.label)} - description={s.description} - variant="description" - className={cn(filteredItems.find((f) => f.name === s.name) ? "" : "hidden")} - onChange={(isChecked) => { - if (isChecked) { - setSelectedScopes((selected) => { - selected.add(s.name); - return new Set(selected); - }); - } else { - setSelectedScopes((selected) => { - selected.delete(s.name); - return new Set(selected); - }); - } - }} - /> - ); - })} -
    -
    - )} -
    - -
    - {scopes.error} - -
    -
    - ); -} diff --git a/apps/webapp/app/components/integrations/connectionType.ts b/apps/webapp/app/components/integrations/connectionType.ts deleted file mode 100644 index bd2359c7c2..0000000000 --- a/apps/webapp/app/components/integrations/connectionType.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ConnectionType } from "@trigger.dev/database"; - -export function connectionType(type: ConnectionType) { - switch (type) { - case "DEVELOPER": - return "Developer"; - case "EXTERNAL": - return "Your users"; - } -} diff --git a/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx b/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx deleted file mode 100644 index c53a85088d..0000000000 --- a/apps/webapp/app/components/jobs/DeleteJobModalContent.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useFetcher } from "@remix-run/react"; -import { useEffect } from "react"; -import { loader } from "~/routes/resources.jobs.$jobId"; -import { cn } from "~/utils/cn"; -import { JobEnvironment, JobStatusTable } from "../JobsStatusTable"; -import { Button } from "../primitives/Buttons"; -import { Header1, Header2 } from "../primitives/Headers"; -import { NamedIcon } from "../primitives/NamedIcon"; -import { Paragraph } from "../primitives/Paragraph"; -import { Spinner } from "../primitives/Spinner"; -import { TextLink } from "../primitives/TextLink"; -import { useTypedFetcher } from "remix-typedjson"; - -export function DeleteJobDialog({ id, title, slug }: { id: string; title: string; slug: string }) { - const fetcher = useTypedFetcher(); - useEffect(() => { - fetcher.load(`/resources/jobs/${id}`); - }, [id]); - - const isLoading = fetcher.state === "loading" || fetcher.state === "submitting"; - - if (isLoading || !fetcher.data) { - return ( -
    -
    - {title} - ID: {slug} -
    - -
    - ); - } else { - return ( - - ); - } -} - -type DeleteJobDialogContentProps = { - id: string; - title: string; - slug: string; - environments: JobEnvironment[]; - redirectTo?: string; -}; - -export function DeleteJobDialogContent({ - title, - slug, - environments, - id, - redirectTo, -}: DeleteJobDialogContentProps) { - const canDelete = environments.every((environment) => !environment.enabled); - const fetcher = useFetcher(); - - const isLoading = - fetcher.state === "submitting" || - (fetcher.state === "loading" && fetcher.formMethod === "DELETE"); - - return ( -
    -
    - {title} - ID: {slug} -
    - - - - {canDelete - ? "Are you sure you want to delete this Job?" - : "You can't delete this Job until all env are disabled"} - - - {canDelete ? ( - <> - This will permanently delete the Job{" "} - {title}. This includes the deletion of - all Run history. This cannot be undone. - - ) : ( - <> - This Job is still active in an environment. You need to disable it in your Job code - first before it can be deleted.{" "} - - Learn how to disable a Job - - . - - )} - - {canDelete ? ( - - - - ) : ( - - )} -
    - ); -} diff --git a/apps/webapp/app/components/jobs/JobSkeleton.tsx b/apps/webapp/app/components/jobs/JobSkeleton.tsx deleted file mode 100644 index 3b9d48d030..0000000000 --- a/apps/webapp/app/components/jobs/JobSkeleton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export function JobSkeleton() { - return ( -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - ); -} diff --git a/apps/webapp/app/components/jobs/JobStatusBadge.tsx b/apps/webapp/app/components/jobs/JobStatusBadge.tsx deleted file mode 100644 index a9de0c1464..0000000000 --- a/apps/webapp/app/components/jobs/JobStatusBadge.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ActiveBadge, MissingIntegrationBadge, NewBadge } from "../ActiveBadge"; - -type JobStatusBadgeProps = { - enabled: boolean; - hasIntegrationsRequiringAction: boolean; - hasRuns: boolean; - badgeSize?: "small" | "normal"; -}; - -export function JobStatusBadge({ - enabled, - hasIntegrationsRequiringAction, - hasRuns, - badgeSize = "normal", -}: JobStatusBadgeProps) { - if (!enabled) { - return ; - } - - if (hasIntegrationsRequiringAction) { - return ; - } - - if (!hasRuns) { - return ; - } - - return ; -} diff --git a/apps/webapp/app/components/jobs/JobsTable.tsx b/apps/webapp/app/components/jobs/JobsTable.tsx deleted file mode 100644 index 451fa9b868..0000000000 --- a/apps/webapp/app/components/jobs/JobsTable.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { JobRunStatus } from "~/models/job.server"; -import { ProjectJob } from "~/presenters/JobListPresenter.server"; -import { jobPath, jobTestPath } from "~/utils/pathBuilder"; -import { Button } from "../primitives/Buttons"; -import { DateTime } from "../primitives/DateTime"; -import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "../primitives/Dialog"; -import { LabelValueStack } from "../primitives/LabelValueStack"; -import { NamedIcon } from "../primitives/NamedIcon"; -import { Paragraph } from "../primitives/Paragraph"; -import { PopoverMenuItem } from "../primitives/Popover"; -import { - Table, - TableBlankRow, - TableBody, - TableCell, - TableCellMenu, - TableHeader, - TableHeaderCell, - TableRow, -} from "../primitives/Table"; -import { SimpleTooltip } from "../primitives/Tooltip"; -import { runStatusTitle } from "../runs/RunStatuses"; -import { DeleteJobDialog, DeleteJobDialogContent } from "./DeleteJobModalContent"; -import { JobStatusBadge } from "./JobStatusBadge"; - -export function JobsTable({ jobs, noResultsText }: { jobs: ProjectJob[]; noResultsText: string }) { - const organization = useOrganization(); - - return ( - - - - Job - ID - Integrations - Properties - Last run - Status - Go to page - - - - {jobs.length > 0 ? ( - jobs.map((job) => { - const path = jobPath(organization, { slug: job.projectSlug }, job); - return ( - - - - - - {" "} - Dynamic: {job.event.title} - - ) : ( - job.event.title - ) - } - variant="primary" - /> - - - - - - - {job.integrations.map((integration) => ( - - - {integration.setupStatus === "MISSING_FIELDS" && ( - - )} - - } - content={ -
    -

    - {integration.setupStatus === "MISSING_FIELDS" && - "This integration requires configuration"} -

    -

    - {integration.title}: {integration.key} -

    -
    - } - /> - ))} -
    - - {job.properties && ( - - {job.properties.map((property, index) => ( - - ))} - - } - content={ -
    - {job.properties.map((property, index) => ( - - ))} -
    - } - /> - )} -
    - - {job.lastRun ? ( - - {runStatusTitle(job.lastRun.status)} - - } - value={ - - - - } - /> - ) : ( - - )} - - - - - - - - - - - - - - Delete Job - - - - -
    - ); - }) - ) : ( - - - {noResultsText} - - - )} -
    -
    - ); -} - -function classForJobStatus(status: JobRunStatus) { - switch (status) { - case "FAILURE": - case "TIMED_OUT": - case "WAITING_ON_CONNECTIONS": - case "PENDING": - case "UNRESOLVED_AUTH": - case "INVALID_PAYLOAD": - return "text-rose-500"; - default: - return ""; - } -} diff --git a/apps/webapp/app/components/layout/AppLayout.tsx b/apps/webapp/app/components/layout/AppLayout.tsx index e6345204ac..fc1fb71541 100644 --- a/apps/webapp/app/components/layout/AppLayout.tsx +++ b/apps/webapp/app/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ import { useOptionalOrganization } from "~/hooks/useOrganizations"; import { cn } from "~/utils/cn"; -import { useShowUpgradePrompt } from "../billing/v3/UpgradePrompt"; +import { useShowUpgradePrompt } from "../billing/UpgradePrompt"; /** This container is used to surround the entire app, it correctly places the nav bar */ export function AppContainer({ children }: { children: React.ReactNode }) { diff --git a/apps/webapp/app/components/layout/app-container-gradient.svg b/apps/webapp/app/components/layout/app-container-gradient.svg deleted file mode 100644 index f9f710d393..0000000000 --- a/apps/webapp/app/components/layout/app-container-gradient.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/apps/webapp/app/components/navigation/AccountSideMenu.tsx b/apps/webapp/app/components/navigation/AccountSideMenu.tsx index ee2518eb1b..4955d380f9 100644 --- a/apps/webapp/app/components/navigation/AccountSideMenu.tsx +++ b/apps/webapp/app/components/navigation/AccountSideMenu.tsx @@ -1,4 +1,4 @@ -import { ShieldCheckIcon } from "@heroicons/react/20/solid"; +import { ShieldCheckIcon, UserCircleIcon } from "@heroicons/react/20/solid"; import { ArrowLeftIcon } from "@heroicons/react/24/solid"; import { User } from "@trigger.dev/database"; import { useFeatures } from "~/hooks/useFeatures"; @@ -36,7 +36,7 @@ export function AccountSideMenu({ user }: { user: User }) {
    - {project.version === "V2" ? ( - - ) : ( - - )} +
    - + - {project.version === "V2" && ( - - )} @@ -171,41 +153,28 @@ export function SideMenu({ user, project, organization, organizations }: SideMen activeIconColor="text-amber-500" data-action="team" /> - {organization.projects.some((proj) => proj.version === "V3") && isManagedCloud && ( - <> - - - - )} - {organization.projects.some((proj) => proj.version === "V2") && ( - - )} + +
    - {isV3Project && isFreeV3User && ( + {isFreeV3User && ( {p.name} - {p.version === "V2" && ( - - v2 - - )}
    } isSelected={isSelected} - icon="folder" + icon={FolderIcon} /> ); }) @@ -285,14 +249,14 @@ function ProjectSelector({ )}
    ))}
    - +
    @@ -361,67 +325,6 @@ function UserMenu({ user }: { user: SideMenuUser }) { ); } -function V2ProjectSideMenu({ - project, - organization, -}: { - project: SideMenuProject; - organization: MatchedOrganization; -}) { - return ( - <> - - - - - - - - - - - ); -} - function V3ProjectSideMenu({ project, organization, @@ -441,7 +344,7 @@ function V3ProjectSideMenu({ /> @@ -504,7 +407,7 @@ function V3ProjectSideMenu({ /> ; + icon?: React.ComponentType; activeIconColor?: string; inactiveIconColor?: string; - trailingIcon?: IconNames | React.ComponentType; + trailingIcon?: React.ComponentType; trailingIconClassName?: string; name: string; to: string; diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 138b6ed5a4..9697b77699 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -2,7 +2,6 @@ import { Link, LinkProps, NavLink, NavLinkProps } from "@remix-run/react"; import React, { forwardRef, ReactNode, useImperativeHandle, useRef } from "react"; import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; import { cn } from "~/utils/cn"; -import { IconNamesOrString, NamedIcon } from "./NamedIcon"; import { ShortcutKey } from "./ShortcutKey"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip"; @@ -165,8 +164,8 @@ const allVariants = { export type ButtonContentPropsType = { children?: React.ReactNode; - LeadingIcon?: React.ComponentType | IconNamesOrString; - TrailingIcon?: React.ComponentType | IconNamesOrString; + LeadingIcon?: React.ComponentType; + TrailingIcon?: React.ComponentType; trailingIconClassName?: string; leadingIconClassName?: string; fullWidth?: boolean; @@ -220,27 +219,16 @@ export function ButtonContent(props: ButtonContentPropsType) { iconSpacing )} > - {LeadingIcon && - (typeof LeadingIcon === "string" ? ( - - ) : ( - - ))} + {LeadingIcon && ( + + )} {text && (typeof text === "string" ? ( @@ -256,27 +244,16 @@ export function ButtonContent(props: ButtonContentPropsType) { props.shortcutPosition === "before-trailing-icon" && renderShortcutKey()} - {TrailingIcon && - (typeof TrailingIcon === "string" ? ( - - ) : ( - - ))} + {TrailingIcon && ( + + )} {shortcut && !tooltip && diff --git a/apps/webapp/app/components/primitives/ClipboardField.tsx b/apps/webapp/app/components/primitives/ClipboardField.tsx index 00605bd18c..dcb6a87879 100644 --- a/apps/webapp/app/components/primitives/ClipboardField.tsx +++ b/apps/webapp/app/components/primitives/ClipboardField.tsx @@ -2,7 +2,6 @@ import { CheckIcon } from "@heroicons/react/20/solid"; import { useCallback, useEffect, useRef, useState } from "react"; import { cn } from "~/utils/cn"; import { Button } from "./Buttons"; -import { IconNames, NamedIcon } from "./NamedIcon"; import { ClipboardCheckIcon, ClipboardIcon } from "lucide-react"; const variants = { @@ -74,7 +73,7 @@ type ClipboardFieldProps = { secure?: boolean | string; variant: keyof typeof variants; className?: string; - icon?: IconNames | React.ReactNode; + icon?: React.ReactNode; iconButton?: boolean; fullWidth?: boolean; }; @@ -120,7 +119,7 @@ export function ClipboardField({ onClick={() => inputIcon.current && inputIcon.current.focus()} className={cn(iconPosition, "flex items-center")} > - {typeof icon === "string" ? : icon} + {icon} )} - + {children} diff --git a/apps/webapp/app/components/primitives/FormTitle.tsx b/apps/webapp/app/components/primitives/FormTitle.tsx index cc2f698ca5..96509dd34c 100644 --- a/apps/webapp/app/components/primitives/FormTitle.tsx +++ b/apps/webapp/app/components/primitives/FormTitle.tsx @@ -1,7 +1,5 @@ import { cn } from "~/utils/cn"; import { Header1 } from "./Headers"; -import type { IconNames } from "./NamedIcon"; -import { NamedIcon } from "./NamedIcon"; import { Paragraph } from "./Paragraph"; export function FormTitle({ @@ -13,7 +11,7 @@ export function FormTitle({ }: { title: React.ReactNode; description?: React.ReactNode; - LeadingIcon?: IconNames | React.ReactNode; + LeadingIcon?: React.ReactNode; divide?: boolean; className?: string; }) { @@ -26,15 +24,7 @@ export function FormTitle({ )} >
    - {LeadingIcon && ( -
    - {typeof LeadingIcon === "string" ? ( - - ) : ( - LeadingIcon - )} -
    - )} + {LeadingIcon &&
    {LeadingIcon}
    } {title}
    {description && {description}} diff --git a/apps/webapp/app/components/primitives/Help.tsx b/apps/webapp/app/components/primitives/Help.tsx deleted file mode 100644 index 9f9ad12737..0000000000 --- a/apps/webapp/app/components/primitives/Help.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Header2 } from "./Headers"; -import { NamedIcon } from "./NamedIcon"; -import { Button } from "./Buttons"; -import gradientPath from "./help-gradient.svg"; -import { cn } from "~/utils/cn"; - -type HelpContextValue = { - open: boolean; - allowDismissing: boolean; - setOpen: (open: boolean) => void; -}; - -const HelpContext = React.createContext({ - open: false, - setOpen: () => {}, - allowDismissing: true, -}); - -type HelpProps = { - defaultOpen?: boolean; - allowDismissing?: boolean; - children?: React.ReactNode | ((open: boolean) => React.ReactNode); -}; - -function useHelp() { - return React.useContext(HelpContext); -} - -export function Help({ defaultOpen, allowDismissing = true, children }: HelpProps) { - const [open, setOpen] = React.useState(defaultOpen || false); - - return ( - - {typeof children === "function" ? children(open) : children} - - ); -} - -export function HelpTrigger({ title }: { title: string }) { - const { open, setOpen } = useHelp(); - - return open ? ( - <> - ) : ( - - ); -} - -export function HelpContent({ - title, - className, - children, -}: { - title: string; - className?: string; - children: React.ReactNode; -}) { - const { open, setOpen, allowDismissing } = useHelp(); - - return ( - <> - {open && ( -
    -
    -
    - - {title} -
    - {allowDismissing && ( - - )} -
    - -
    - {children} -
    -
    - )} - - ); -} diff --git a/apps/webapp/app/components/primitives/Icon.tsx b/apps/webapp/app/components/primitives/Icon.tsx index 5d49770c4d..1add80a68f 100644 --- a/apps/webapp/app/components/primitives/Icon.tsx +++ b/apps/webapp/app/components/primitives/Icon.tsx @@ -1,13 +1,9 @@ import React, { FunctionComponent, createElement } from "react"; import { cn } from "~/utils/cn"; -import { IconNamesOrString, NamedIcon } from "./NamedIcon"; -export type RenderIcon = - | IconNamesOrString - | FunctionComponent<{ className?: string }> - | React.ReactNode; +export type RenderIcon = FunctionComponent<{ className?: string }> | React.ReactNode; -type IconProps = { +export type IconProps = { icon?: RenderIcon; className?: string; }; @@ -16,10 +12,6 @@ type IconProps = { export function Icon(props: IconProps) { if (!props.icon) return null; - if (typeof props.icon === "string") { - return } />; - } - if (typeof props.icon === "function") { const Icon = props.icon; return ; diff --git a/apps/webapp/app/components/primitives/NamedIcon.tsx b/apps/webapp/app/components/primitives/NamedIcon.tsx deleted file mode 100644 index 9148fa3d60..0000000000 --- a/apps/webapp/app/components/primitives/NamedIcon.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import { - ArrowTopRightOnSquareIcon, - ExclamationTriangleIcon, - InformationCircleIcon, - StopIcon, -} from "@heroicons/react/20/solid"; -import { - ArrowLeftIcon, - ArrowPathIcon, - ArrowRightIcon, - BeakerIcon, - BellAlertIcon, - BoltIcon, - BookOpenIcon, - BuildingOffice2Icon, - CalendarDaysIcon, - ChatBubbleLeftEllipsisIcon, - CheckCircleIcon, - CheckIcon, - ChevronDownIcon, - ChevronLeftIcon, - ChevronRightIcon, - ChevronUpIcon, - ClipboardDocumentCheckIcon, - ClockIcon, - CloudIcon, - CodeBracketSquareIcon, - Cog8ToothIcon, - CreditCardIcon, - EnvelopeIcon, - EyeIcon, - FingerPrintIcon, - FlagIcon, - FolderIcon, - GlobeAltIcon, - HandRaisedIcon, - HeartIcon, - HomeIcon, - KeyIcon, - LightBulbIcon, - ListBulletIcon, - MagnifyingGlassIcon, - PlusIcon, - PlusSmallIcon, - QrCodeIcon, - Square2StackIcon, - SquaresPlusIcon, - StarIcon, - TrashIcon, - UserCircleIcon, - UserGroupIcon, - UserIcon, - UserPlusIcon, - WindowIcon, - WrenchScrewdriverIcon, - XCircleIcon, - XMarkIcon, -} from "@heroicons/react/24/solid"; -import { CompanyIcon, hasIcon } from "@trigger.dev/companyicons"; -import { ActivityIcon, HourglassIcon } from "lucide-react"; -import { DynamicTriggerIcon } from "~/assets/icons/DynamicTriggerIcon"; -import { EndpointIcon } from "~/assets/icons/EndpointIcon"; -import { ErrorIcon } from "~/assets/icons/ErrorIcon"; -import { OneTreeIcon } from "~/assets/icons/OneTreeIcon"; -import { RunsIcon } from "~/assets/icons/RunsIcon"; -import { SaplingIcon } from "~/assets/icons/SaplingIcon"; -import { ScheduleIcon } from "~/assets/icons/ScheduleIcon"; -import { TwoTreesIcon } from "~/assets/icons/TwoTreesIcon"; -import { WebhookIcon } from "~/assets/icons/WebhookIcon"; -import { cn } from "~/utils/cn"; -import { tablerIcons } from "~/utils/tablerIcons"; -import { LogoIcon } from "../LogoIcon"; -import { Spinner } from "./Spinner"; -import tablerSpritePath from "./tabler-sprite.svg"; - -const icons = { - account: (className: string) => , - active: (className: string) => , - "arrow-right": (className: string) => , - "arrow-left": (className: string) => , - background: (className: string) => , - beaker: (className: string) => , - bell: (className: string) => , - billing: (className: string) => , - browser: (className: string) => , - calendar: (className: string) => ( - - ), - check: (className: string) => , - "chevron-down": (className: string) => ( - - ), - "chevron-up": (className: string) => ( - - ), - "chevron-left": (className: string) => ( - - ), - "chevron-right": (className: string) => ( - - ), - countdown: (className: string) => , - clock: (className: string) => , - close: (className: string) => , - "connection-alert": (className: string) => ( - - ), - docs: (className: string) => , - dynamic: (className: string) => , - error: (className: string) => , - "external-link": (className: string) => ( - - ), - flag: (className: string) => , - folder: (className: string) => , - envelope: (className: string) => , - environment: (className: string) => , - eye: (className: string) => , - globe: (className: string) => , - "hand-raised": (className: string) => ( - - ), - heart: (className: string) => , - house: (className: string) => , - id: (className: string) => , - inactive: (className: string) => , - info: (className: string) => , - integration: (className: string) => ( - - ), - "invite-member": (className: string) => ( - - ), - job: (className: string) => ( - - ), - key: (className: string) => , - lightbulb: (className: string) => , - "clipboard-checked": (className: string) => ( - - ), - list: (className: string) => , - log: (className: string) => ( - - ), - "logo-icon": (className: string) => , - organization: (className: string) => ( - - ), - plus: (className: string) => , - "plus-small": (className: string) => ( - - ), - property: (className: string) => , - pulse: (className: string) => , - "qr-code": (className: string) => , - refresh: (className: string) => , - sapling: (className: string) => , - search: (className: string) => ( - - ), - settings: (className: string) => , - spinner: (className: string) => , - "spinner-white": (className: string) => , - "spinner-dark": (className: string) => , - squares: (className: string) => ( - - ), - star: (className: string) => , - stop: (className: string) => , - team: (className: string) => , - "trash-can": (className: string) => , - tree: (className: string) => , - trees: (className: string) => , - trigger: (className: string) => , - user: (className: string) => , - warning: (className: string) => ( - - ), - //triggers - "custom-event": (className: string) => ( - - ), - "register-source": (className: string) => ( - - ), - "schedule-interval": (className: string) => ( - - ), - "schedule-cron": (className: string) => ( - - ), - "schedule-dynamic": (className: string) => ( - - ), - webhook: (className: string) => , - endpoint: (className: string) => , - "http-endpoint": (className: string) => ( - - ), - runs: (className: string) => , -}; - -export type IconNames = keyof typeof icons; -export type IconNamesOrString = IconNames | (string & {}); -export const iconNames = Object.keys(icons) as IconNames[]; - -export function NamedIcon({ - name, - className, - fallback, -}: { - name: IconNamesOrString; - className: string; - fallback?: JSX.Element; -}) { - if (Object.keys(icons).includes(name)) { - return icons[name as IconNames](className); - } - - if (hasIcon(name)) { - return ( - - - - ); - } - - if (tablerIcons.has("tabler-" + name)) { - return ; - } else if (name.startsWith("tabler-") && tablerIcons.has(name)) { - return ; - } - - if (name === "supabase-management") { - return ; - } - - if (fallback) { - return fallback; - } - - //default fallback icon - return ; -} - -export function NamedIconInBox({ - name, - className, - fallback, - iconClassName, -}: { - name: string; - className?: string; - fallback?: JSX.Element; - iconClassName?: string; -}) { - return ( -
    - -
    - ); -} - -export function TablerIcon({ name, className }: { name: string; className?: string }) { - return ( - - - - ); -} diff --git a/apps/webapp/app/components/primitives/PageHeader.tsx b/apps/webapp/app/components/primitives/PageHeader.tsx index d06692ed9b..e9749f84eb 100644 --- a/apps/webapp/app/components/primitives/PageHeader.tsx +++ b/apps/webapp/app/components/primitives/PageHeader.tsx @@ -1,16 +1,10 @@ -import { ArrowUpRightIcon } from "@heroicons/react/20/solid"; import { Link, useNavigation } from "@remix-run/react"; +import { ReactNode } from "react"; import { useOptionalOrganization } from "~/hooks/useOrganizations"; -import { cn } from "~/utils/cn"; -import { UpgradePrompt, useShowUpgradePrompt } from "../billing/v3/UpgradePrompt"; +import { UpgradePrompt, useShowUpgradePrompt } from "../billing/UpgradePrompt"; import { BreadcrumbIcon } from "./BreadcrumbIcon"; -import { LinkButton } from "./Buttons"; import { Header2 } from "./Headers"; import { LoadingBarDivider } from "./LoadingBarDivider"; -import { NamedIcon } from "./NamedIcon"; -import { Paragraph } from "./Paragraph"; -import { Tabs, TabsProps } from "./Tabs"; -import { ReactNode } from "react"; type WithChildren = { children: React.ReactNode; @@ -64,76 +58,3 @@ export function PageTitle({ title, backButton }: PageTitleProps) { export function PageAccessories({ children }: WithChildren) { return
    {children}
    ; } - -export function PageInfoRow({ children, className }: WithChildren) { - return
    {children}
    ; -} - -export function PageInfoGroup({ - children, - alignment = "left", -}: WithChildren & { alignment?: "left" | "right" }) { - return ( -
    - {children} -
    - ); -} - -export function PageInfoProperty({ - icon, - label, - value, - to, -}: { - icon?: string | React.ReactNode; - label?: string; - value?: React.ReactNode; - to?: string; -}) { - if (to === undefined) { - return ; - } - - return ( - - - - ); -} - -function PageInfoPropertyContent({ - icon, - label, - value, -}: { - icon?: string | React.ReactNode; - label?: string; - value?: React.ReactNode; -}) { - return ( -
    - {icon && typeof icon === "string" ? : icon} - {label && ( - - {label} - {value !== undefined && ":"} - - )} - {value !== undefined && {value}} -
    - ); -} - -export function PageTabs(props: TabsProps) { - return ( -
    - -
    - ); -} diff --git a/apps/webapp/app/components/primitives/Paragraph.tsx b/apps/webapp/app/components/primitives/Paragraph.tsx index d545863bb3..971ce3b4e5 100644 --- a/apps/webapp/app/components/primitives/Paragraph.tsx +++ b/apps/webapp/app/components/primitives/Paragraph.tsx @@ -1,6 +1,4 @@ -import { Link } from "@remix-run/react"; import { cn } from "~/utils/cn"; -import { IconNamesOrString, NamedIcon } from "./NamedIcon"; const paragraphVariants = { base: { diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 95c4d92e8a..3a05575d4f 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -8,6 +8,7 @@ import { type ButtonContentPropsType, LinkButton } from "./Buttons"; import { Paragraph, type ParagraphVariant } from "./Paragraph"; import { ShortcutKey } from "./ShortcutKey"; import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { CheckIcon } from "@heroicons/react/20/solid"; const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; @@ -58,7 +59,7 @@ function PopoverMenuItem({ leadingIconClassName, }: { to: string; - icon: string | React.ComponentType; + icon: React.ComponentType; title: React.ReactNode; isSelected?: boolean; variant?: ButtonContentPropsType; @@ -72,7 +73,7 @@ function PopoverMenuItem({ leadingIconClassName={leadingIconClassName} fullWidth textAlignLeft - TrailingIcon={isSelected ? "check" : undefined} + TrailingIcon={isSelected ? CheckIcon : undefined} className={cn( "group-hover:bg-charcoal-700", isSelected ? "bg-charcoal-750 group-hover:bg-charcoal-600/50" : undefined diff --git a/apps/webapp/app/components/primitives/Sheet.tsx b/apps/webapp/app/components/primitives/Sheet.tsx index 35f7536429..49ad15fe0e 100644 --- a/apps/webapp/app/components/primitives/Sheet.tsx +++ b/apps/webapp/app/components/primitives/Sheet.tsx @@ -5,8 +5,8 @@ import type { VariantProps } from "class-variance-authority"; import { cva } from "class-variance-authority"; import * as React from "react"; import { cn } from "~/utils/cn"; -import { NamedIcon } from "./NamedIcon"; import { ShortcutKey } from "./ShortcutKey"; +import { XMarkIcon } from "@heroicons/react/20/solid"; const Sheet = SheetPrimitive.Root; @@ -156,7 +156,7 @@ const SheetContent = React.forwardRef<
    - + Close diff --git a/apps/webapp/app/components/primitives/SheetV3.tsx b/apps/webapp/app/components/primitives/SheetV3.tsx index 2bac7e9cb9..bb205f8b42 100644 --- a/apps/webapp/app/components/primitives/SheetV3.tsx +++ b/apps/webapp/app/components/primitives/SheetV3.tsx @@ -1,9 +1,8 @@ +import { XMarkIcon } from "@heroicons/react/20/solid"; import * as SheetPrimitive from "@radix-ui/react-dialog"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import { cn } from "~/utils/cn"; -import { Header2 } from "./Headers"; -import { NamedIcon } from "./NamedIcon"; import { ShortcutKey } from "./ShortcutKey"; const Sheet = SheetPrimitive.Root; @@ -93,7 +92,7 @@ const SheetTitle = React.forwardRef< {children} - + Close diff --git a/apps/webapp/app/components/primitives/Spinner.tsx b/apps/webapp/app/components/primitives/Spinner.tsx index b9a6961236..a9182b46ce 100644 --- a/apps/webapp/app/components/primitives/Spinner.tsx +++ b/apps/webapp/app/components/primitives/Spinner.tsx @@ -72,3 +72,7 @@ export function ButtonSpinner() { /> ); } + +export function SpinnerWhite({ className }: { className?: string }) { + return ; +} diff --git a/apps/webapp/app/components/primitives/TextLink.tsx b/apps/webapp/app/components/primitives/TextLink.tsx index e5c8e7d487..e4314d4b0f 100644 --- a/apps/webapp/app/components/primitives/TextLink.tsx +++ b/apps/webapp/app/components/primitives/TextLink.tsx @@ -1,6 +1,6 @@ import { Link } from "@remix-run/react"; -import { IconNamesOrString, NamedIcon } from "./NamedIcon"; import { cn } from "~/utils/cn"; +import { Icon, RenderIcon } from "./Icon"; const variations = { primary: @@ -13,7 +13,7 @@ type TextLinkProps = { href?: string; to?: string; className?: string; - trailingIcon?: IconNamesOrString; + trailingIcon?: RenderIcon; trailingIconClassName?: string; variant?: keyof typeof variations; children: React.ReactNode; @@ -33,16 +33,12 @@ export function TextLink({ return to ? ( {children}{" "} - {trailingIcon && ( - - )} + {trailingIcon && } ) : href ? ( {children}{" "} - {trailingIcon && ( - - )} + {trailingIcon && } ) : ( Need to define a path or href diff --git a/apps/webapp/app/components/primitives/help-gradient.svg b/apps/webapp/app/components/primitives/help-gradient.svg deleted file mode 100644 index 0b17ade449..0000000000 --- a/apps/webapp/app/components/primitives/help-gradient.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/apps/webapp/app/components/run/RunCard.tsx b/apps/webapp/app/components/run/RunCard.tsx deleted file mode 100644 index 2dfcbf5f63..0000000000 --- a/apps/webapp/app/components/run/RunCard.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import type { DisplayProperty, StyleName } from "@trigger.dev/core"; -import { formatDuration } from "@trigger.dev/core/v3"; -import { motion } from "framer-motion"; -import { HourglassIcon } from "lucide-react"; -import { ReactNode, useEffect, useState } from "react"; -import { CodeBlock } from "~/components/code/CodeBlock"; -import { Callout } from "~/components/primitives/Callout"; -import { LabelValueStack } from "~/components/primitives/LabelValueStack"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { cn } from "~/utils/cn"; - -type RunPanelProps = { - selected?: boolean; - children: React.ReactNode; - onClick?: () => void; - className?: string; - styleName?: StyleName; -}; - -export function RunPanel({ - selected = false, - children, - onClick, - className, - styleName = "normal", -}: RunPanelProps) { - return ( -
    onClick && onClick()} - > - {children} -
    - ); -} - -type RunPanelHeaderProps = { - icon: React.ReactNode; - title: React.ReactNode; - accessory?: React.ReactNode; - styleName?: StyleName; -}; - -export function RunPanelHeader({ - icon, - title, - accessory, - styleName = "normal", -}: RunPanelHeaderProps) { - return ( -
    -
    - {typeof icon === "string" ? : icon} - {typeof title === "string" ? {title} : title} -
    -
    {accessory}
    -
    - ); -} - -type RunPanelIconTitleProps = { - icon?: string | null; - title: string; -}; - -export function RunPanelIconTitle({ icon, title }: RunPanelIconTitleProps) { - return ( -
    - {icon && } - {title} -
    - ); -} - -export function RunPanelBody({ children }: { children: React.ReactNode }) { - return
    {children}
    ; -} - -const variantClasses: Record = { - log: "", - error: "text-rose-500", - warn: "text-yellow-500", - info: "", - debug: "", -}; - -export function RunPanelDescription({ text, variant }: { text: string; variant?: string }) { - return ( - - {text} - - ); -} - -export function RunPanelError({ - text, - error, - stackTrace, -}: { - text: string; - error?: string; - stackTrace?: string; -}) { - return ( -
    - - {text} - - {error && } - {stackTrace && } -
    - ); -} - -export function RunPanelIconSection({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return
    {children}
    ; -} - -export function RunPanelDivider() { - return
    ; -} - -export function RunPanelIconProperty({ - icon, - label, - value, -}: { - icon: ReactNode; - label: string; - value: ReactNode; -}) { - return ( -
    -
    - {typeof icon === "string" ? : icon} -
    -
    - {label} - {value} -
    -
    - ); -} - -export function RunPanelProperties({ - properties, - className, - layout = "horizontal", -}: { - properties: DisplayProperty[]; - className?: string; - layout?: "horizontal" | "vertical"; -}) { - return ( -
    - {properties.map(({ label, text, url }, index) => ( - - ))} -
    - ); -} - -export function TaskSeparator({ depth }: { depth: number }) { - return ( -
    - ); -} - -const updateInterval = 100; - -export function UpdatingDuration({ start, end }: { start?: Date; end?: Date }) { - const [now, setNow] = useState(); - - useEffect(() => { - if (end) return; - - const interval = setInterval(() => { - setNow(new Date()); - }, updateInterval); - - return () => clearInterval(interval); - }, [end]); - - return ( - - {formatDuration(start, end || now, { - style: "short", - maxDecimalPoints: 0, - })} - - ); -} - -export function UpdatingDelay({ delayUntil }: { delayUntil: Date }) { - const [now, setNow] = useState(); - - useEffect(() => { - const interval = setInterval(() => { - const date = new Date(); - if (date > delayUntil) { - setNow(delayUntil); - return; - } - setNow(date); - }, updateInterval); - - return () => clearInterval(interval); - }, [delayUntil]); - - return ( - - - - } - label="Delay finishes in" - value={formatDuration(now, delayUntil, { - style: "long", - maxDecimalPoints: 0, - })} - /> - ); -} diff --git a/apps/webapp/app/components/run/RunCompletedDetail.tsx b/apps/webapp/app/components/run/RunCompletedDetail.tsx deleted file mode 100644 index f11a9bd563..0000000000 --- a/apps/webapp/app/components/run/RunCompletedDetail.tsx +++ /dev/null @@ -1,64 +0,0 @@ -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 { MatchedRun } from "~/hooks/useRun"; -import { - RunPanel, - RunPanelBody, - RunPanelDivider, - RunPanelError, - RunPanelHeader, - RunPanelIconProperty, - RunPanelIconSection, -} from "./RunCard"; -import { formatDuration } from "@trigger.dev/core/v3"; - -export function RunCompletedDetail({ run }: { run: MatchedRun }) { - return ( - - } - title={ - - - - } - /> - - - {run.startedAt && ( - } - /> - )} - {run.completedAt && ( - } - /> - )} - {run.startedAt && run.completedAt && ( - - )} - - - {run.error && } - {run.output ? ( - - ) : ( - run.output === null && This run returned nothing - )} - - - ); -} diff --git a/apps/webapp/app/components/run/RunOverview.tsx b/apps/webapp/app/components/run/RunOverview.tsx deleted file mode 100644 index cdaf64f44f..0000000000 --- a/apps/webapp/app/components/run/RunOverview.tsx +++ /dev/null @@ -1,435 +0,0 @@ -import { conform, useForm } from "@conform-to/react"; -import { parse } from "@conform-to/zod"; -import { PlayIcon } from "@heroicons/react/20/solid"; -import { BoltIcon } from "@heroicons/react/24/solid"; -import { - Form, - Outlet, - useActionData, - useLocation, - useNavigate, - useNavigation, -} from "@remix-run/react"; -import { RuntimeEnvironmentType, User } from "@trigger.dev/database"; -import { useMemo } from "react"; -import { usePathName } from "~/hooks/usePathName"; -import type { RunBasicStatus } from "~/models/jobRun.server"; -import { ViewRun } from "~/presenters/RunPresenter.server"; -import { cancelSchema } from "~/routes/resources.runs.$runId.cancel"; -import { schema } from "~/routes/resources.runs.$runId.rerun"; -import { cn } from "~/utils/cn"; -import { runCompletedPath, runTaskPath, runTriggerPath } from "~/utils/pathBuilder"; -import { CodeBlock } from "../code/CodeBlock"; -import { EnvironmentLabel } from "../environments/EnvironmentLabel"; -import { PageBody, PageContainer } from "../layout/AppLayout"; -import { Button } from "../primitives/Buttons"; -import { Callout } from "../primitives/Callout"; -import { DateTime } from "../primitives/DateTime"; -import { Header2 } from "../primitives/Headers"; -import { Icon } from "../primitives/Icon"; -import { NamedIcon } from "../primitives/NamedIcon"; -import { - PageAccessories, - NavBar, - PageInfoGroup, - PageInfoProperty, - PageInfoRow, - PageTitle, -} from "../primitives/PageHeader"; -import { Paragraph } from "../primitives/Paragraph"; -import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover"; -import { RunStatusIcon, RunStatusLabel, runStatusTitle } from "../runs/RunStatuses"; -import { - RunPanel, - RunPanelBody, - RunPanelDivider, - RunPanelError, - RunPanelHeader, - RunPanelIconProperty, - RunPanelIconSection, - RunPanelProperties, -} from "./RunCard"; -import { TaskCard } from "./TaskCard"; -import { TaskCardSkeleton } from "./TaskCardSkeleton"; -import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3"; - -type RunOverviewProps = { - run: ViewRun; - trigger: { - icon: string; - title: string; - }; - showRerun: boolean; - paths: { - back: string; - run: string; - runsPath: string; - }; - currentUser: User; -}; - -const taskPattern = /\/tasks\/(.*)/; - -export function RunOverview({ run, trigger, showRerun, paths, currentUser }: RunOverviewProps) { - const navigate = useNavigate(); - const pathName = usePathName(); - - const selectedId = useMemo(() => { - if (pathName.endsWith("/completed")) { - return "completed"; - } - - if (pathName.endsWith("/trigger")) { - return "trigger"; - } - - const taskMatch = pathName.match(taskPattern); - const taskId = taskMatch ? taskMatch[1] : undefined; - if (taskId) { - return taskId; - } - }, [pathName]); - - const usernameForEnv = - currentUser.id !== run.environment.userId ? run.environment.userName : undefined; - - return ( - - - - - {run.isTest && ( - - - Test run - - )} - {showRerun && run.isFinished && ( - - )} - {!run.isFinished && } - - - -
    - - - } - label={"Status"} - value={runStatusTitle(run.status)} - /> - : "Not started yet"} - /> - - } - /> - - } - label={"Execution Time"} - value={formatDurationMilliseconds(run.executionDuration, { style: "short" })} - /> - } - label={"Execution Count"} - value={<>{run.executionCount}} - /> - - - - RUN ID: {run.id} - - - -
    -
    -
    -
    - {run.status === "SUCCESS" && - (run.tasks.length === 0 || run.tasks.every((t) => t.noop)) && ( - - This Run completed but it did not use any Tasks – this can cause unpredictable - results. Read the docs to view the solution. - - )} - Trigger - navigate(runTriggerPath(paths.run))} - > - - - - - -
    -
    - Tasks - - {run.tasks.length > 0 ? ( - run.tasks.map((task, index) => { - const isLast = index === run.tasks.length - 1; - - return ( - { - navigate(runTaskPath(paths.run, taskId)); - }} - isLast={isLast} - depth={0} - {...task} - /> - ); - }) - ) : ( - - )} -
    - {(run.basicStatus === "COMPLETED" || run.basicStatus === "FAILED") && ( -
    - Run Summary - navigate(runCompletedPath(paths.run))} - > - } - title={ - - - - } - /> - - - {run.startedAt && ( - } - /> - )} - {run.completedAt && ( - } - /> - )} - {run.startedAt && run.completedAt && ( - - )} - - - {run.error && ( - - )} - {run.output ? ( - - ) : ( - run.output === null && ( - - This Run returned nothing. - - ) - )} - - -
    - )} -
    - - {/* Detail view */} -
    - Details - {selectedId ? : Select a task or trigger} -
    -
    -
    -
    - ); -} - -function BlankTasks({ status }: { status: RunBasicStatus }) { - switch (status) { - default: - case "COMPLETED": - return There were no tasks for this run.; - case "FAILED": - return No tasks were run.; - case "WAITING": - case "PENDING": - case "RUNNING": - return ( -
    - - Waiting for tasks… - - -
    - ); - } -} - -function RerunPopover({ - runId, - runPath, - runsPath, - environmentType, - status, -}: { - runId: string; - runPath: string; - runsPath: string; - environmentType: RuntimeEnvironmentType; - status: RunBasicStatus; -}) { - const lastSubmission = useActionData(); - - const [form, { successRedirect, failureRedirect }] = useForm({ - id: "rerun", - // TODO: type this - lastSubmission: lastSubmission as any, - onValidate({ formData }) { - return parse(formData, { schema }); - }, - }); - - return ( - - - - - -
    - - - {environmentType === "PRODUCTION" && ( -
    - - This will rerun this Job in your Production environment. - -
    - )} - -
    -
    - - Start a brand new Job run with the same Trigger data as this one. This will re-do - every Task. - - -
    - {status === "FAILED" && ( -
    - - Continue running this Job run from where it left off. This will skip any Task that - has already been completed. - - -
    - )} -
    -
    -
    -
    - ); -} - -export function CancelRun({ runId }: { runId: string }) { - const lastSubmission = useActionData(); - const location = useLocation(); - const navigation = useNavigation(); - - const [form, { redirectUrl }] = useForm({ - id: "cancel-run", - // TODO: type this - lastSubmission: lastSubmission as any, - onValidate({ formData }) { - return parse(formData, { schema: cancelSchema }); - }, - }); - - const isLoading = navigation.state === "submitting" && navigation.formData !== undefined; - - return ( -
    - - -
    - ); -} diff --git a/apps/webapp/app/components/run/TaskAttemptStatus.tsx b/apps/webapp/app/components/run/TaskAttemptStatus.tsx deleted file mode 100644 index df87d16297..0000000000 --- a/apps/webapp/app/components/run/TaskAttemptStatus.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { CheckCircleIcon, ClockIcon, XCircleIcon } from "@heroicons/react/24/solid"; -import type { TaskAttemptStatus } from "@trigger.dev/database"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { Spinner } from "~/components/primitives/Spinner"; -import { cn } from "~/utils/cn"; - -type TaskAttemptStatusProps = { - status: TaskAttemptStatus; - className?: string; -}; - -export function TaskAttemptStatusLabel({ status }: { status: TaskAttemptStatus }) { - return ( - - - - {taskAttemptStatusTitle(status)} - - - ); -} - -export function TaskAttemptStatusIcon({ status, className }: TaskAttemptStatusProps) { - switch (status) { - case "COMPLETED": - return ; - case "PENDING": - return ; - case "STARTED": - return ; - case "ERRORED": - return ; - } -} - -function taskAttemptStatusClassNameColor(status: TaskAttemptStatus): string { - switch (status) { - case "COMPLETED": - return "text-green-500"; - case "PENDING": - return "text-charcoal-400"; - case "STARTED": - return "text-blue-500"; - case "ERRORED": - return "text-rose-500"; - } -} - -function taskAttemptStatusTitle(status: TaskAttemptStatus): string { - switch (status) { - case "COMPLETED": - return "Complete"; - case "PENDING": - return "Scheduled"; - case "STARTED": - return "Running"; - case "ERRORED": - return "Error"; - } -} diff --git a/apps/webapp/app/components/run/TaskCard.tsx b/apps/webapp/app/components/run/TaskCard.tsx deleted file mode 100644 index a0ccc9e3ac..0000000000 --- a/apps/webapp/app/components/run/TaskCard.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { ChevronDownIcon, Square2StackIcon } from "@heroicons/react/24/solid"; -import { AnimatePresence, motion } from "framer-motion"; -import { Fragment, useState } from "react"; -import simplur from "simplur"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { ViewTask } from "~/presenters/RunPresenter.server"; -import { cn } from "~/utils/cn"; -import { - RunPanel, - RunPanelBody, - RunPanelDescription, - RunPanelError, - RunPanelHeader, - RunPanelIconProperty, - RunPanelIconSection, - RunPanelIconTitle, - RunPanelProperties, - TaskSeparator, - UpdatingDelay, - UpdatingDuration, -} from "./RunCard"; -import { TaskStatusIcon } from "./TaskStatus"; -import { formatDuration } from "@trigger.dev/core/v3"; - -type TaskCardProps = ViewTask & { - selectedId?: string; - selectedTask: (id: string) => void; - isLast: boolean; - depth: number; -}; - -export function TaskCard({ - selectedId, - selectedTask, - isLast, - depth, - id, - style, - status, - icon, - name, - startedAt, - completedAt, - description, - displayKey, - connection, - properties, - subtasks, - error, - delayUntil, -}: TaskCardProps) { - const [expanded, setExpanded] = useState(false); - const isSelected = id === selectedId; - - return ( - -
    - selectedTask(id)} styleName={style?.style}> - - ) - } - title={status === "COMPLETED" ? name : } - accessory={ - - - - } - styleName={style?.style} - /> - - {error && } - {description && } - - {displayKey && } - {delayUntil && !completedAt && ( - <> - - - )} - {delayUntil && completedAt && ( - - )} - {connection && ( - - )} - - {properties.length > 0 && ( - - )} - - {subtasks && subtasks.length > 0 && ( - - )} - -
    - {(!isLast || expanded) && } - - {subtasks && - subtasks.length > 0 && - expanded && - subtasks.map((subtask, index) => ( - - - - - - ))} -
    - ); -} diff --git a/apps/webapp/app/components/run/TaskCardSkeleton.tsx b/apps/webapp/app/components/run/TaskCardSkeleton.tsx deleted file mode 100644 index e3708eb87b..0000000000 --- a/apps/webapp/app/components/run/TaskCardSkeleton.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { RunPanel, RunPanelBody, RunPanelHeader } from "./RunCard"; - -export function TaskCardSkeleton() { - return ( - - } - /> - -
    -
    -
    -
    -
    -
    -
    -
    - - - ); -} diff --git a/apps/webapp/app/components/run/TaskDetail.tsx b/apps/webapp/app/components/run/TaskDetail.tsx deleted file mode 100644 index d09df85c0c..0000000000 --- a/apps/webapp/app/components/run/TaskDetail.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { - RunPanel, - RunPanelBody, - RunPanelDescription, - RunPanelDivider, - RunPanelHeader, - RunPanelIconProperty, - RunPanelIconSection, - RunPanelIconTitle, - RunPanelProperties, - UpdatingDelay, - UpdatingDuration, -} from "./RunCard"; -import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer"; -import { cn } from "~/utils/cn"; -import { CodeBlock } from "../code/CodeBlock"; -import { DateTime } from "../primitives/DateTime"; -import { Header3 } from "../primitives/Headers"; -import { Paragraph } from "../primitives/Paragraph"; -import { - TableHeader, - TableRow, - TableHeaderCell, - TableBody, - TableCell, - Table, -} from "../primitives/Table"; -import { TaskAttemptStatusLabel } from "./TaskAttemptStatus"; -import { TaskStatusIcon } from "./TaskStatus"; -import { ClientOnly } from "remix-utils/client-only"; -import { Spinner } from "../primitives/Spinner"; -import type { DetailedTask } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route"; -import { formatDuration } from "@trigger.dev/core/v3"; - -export function TaskDetail({ task }: { task: DetailedTask }) { - const { - name, - description, - icon, - status, - params, - properties, - output, - outputIsUndefined, - style, - attempts, - noop, - } = task; - - const startedAt = task.startedAt ? new Date(task.startedAt) : undefined; - const completedAt = task.completedAt ? new Date(task.completedAt) : undefined; - const delayUntil = task.delayUntil ? new Date(task.delayUntil) : undefined; - - return ( - - } - title={} - accessory={ - - - - } - /> - - - {startedAt && ( - } - /> - )} - {completedAt && ( - } - /> - )} - {delayUntil && !completedAt && ( - <> - } - /> - - - )} - {delayUntil && completedAt && ( - - )} - - - {description && } - {properties.length > 0 && ( -
    - Properties - -
    - )} - - {attempts.length > 1 && ( -
    - Retries - - - - Attempt - Status - Date - Error - - - - {attempts.map((attempt) => ( - - {attempt.number} - - - - - - - {attempt.error} - - ))} - -
    -
    - )} - -
    - Input - {params ? ( - - ) : ( - No input - )} -
    - {!noop && ( -
    - Output - {output && !outputIsUndefined ? ( - }> - {() => } - - ) : ( - No output - )} -
    - )} -
    -
    - ); -} diff --git a/apps/webapp/app/components/run/TaskStatus.tsx b/apps/webapp/app/components/run/TaskStatus.tsx deleted file mode 100644 index 38a4703a72..0000000000 --- a/apps/webapp/app/components/run/TaskStatus.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { TaskStatus } from "@trigger.dev/core"; -import { - CheckCircleIcon, - CheckIcon, - ClockIcon, - NoSymbolIcon, - XCircleIcon, -} from "@heroicons/react/24/solid"; -import { Spinner } from "~/components/primitives/Spinner"; -import { cn } from "~/utils/cn"; - -type TaskStatusIconProps = { - status: TaskStatus; - className: string; - minimal?: boolean; -}; - -export function TaskStatusIcon({ status, className, minimal = false }: TaskStatusIconProps) { - switch (status) { - case "COMPLETED": - return minimal ? ( - - ) : ( - - ); - case "PENDING": - return ; - case "WAITING": - return ; - case "RUNNING": - return ; - case "ERRORED": - return ; - case "CANCELED": - return ; - } -} - -function taskStatusClassNameColor(status: TaskStatus): string { - switch (status) { - case "COMPLETED": - return "text-green-500"; - case "PENDING": - return "text-charcoal-500"; - case "RUNNING": - return "text-blue-500"; - case "WAITING": - return "text-blue-500"; - case "ERRORED": - return "text-rose-500"; - case "CANCELED": - return "text-charcoal-500"; - } -} diff --git a/apps/webapp/app/components/run/TriggerDetail.tsx b/apps/webapp/app/components/run/TriggerDetail.tsx deleted file mode 100644 index 4c7a393936..0000000000 --- a/apps/webapp/app/components/run/TriggerDetail.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { DetailedEvent } from "~/presenters/TriggerDetailsPresenter.server"; -import { CodeBlock } from "../code/CodeBlock"; -import { DateTime } from "../primitives/DateTime"; -import { Header3 } from "../primitives/Headers"; -import { - RunPanel, - RunPanelBody, - RunPanelDivider, - RunPanelHeader, - RunPanelIconProperty, - RunPanelIconSection, - RunPanelProperties, -} from "./RunCard"; -import type { DisplayProperty } from "@trigger.dev/core"; - -export function TriggerDetail({ - trigger, - event, - properties, -}: { - trigger: DetailedEvent; - event: { - title: string; - icon: string; - }; - properties: DisplayProperty[]; -}) { - const { id, name, payload, context, timestamp, deliveredAt } = trigger; - - return ( - - - - - } - /> - {deliveredAt && ( - } - /> - )} - - - {trigger.externalAccount && ( - - )} - - -
    - {properties.length > 0 && ( -
    - Properties - -
    - )} - Payload - - Context - -
    -
    -
    - ); -} diff --git a/apps/webapp/app/components/runs/RunFilters.tsx b/apps/webapp/app/components/runs/RunFilters.tsx deleted file mode 100644 index b70d2e9d4e..0000000000 --- a/apps/webapp/app/components/runs/RunFilters.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { - CheckCircleIcon, - ClockIcon, - ExclamationTriangleIcon, - NoSymbolIcon, - PauseCircleIcon, - TrashIcon, - XCircleIcon, - XMarkIcon, -} from "@heroicons/react/20/solid"; -import { useNavigate } from "@remix-run/react"; -import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; -import { cn } from "~/utils/cn"; -import { EnvironmentLabel } from "../environments/EnvironmentLabel"; -import { Paragraph } from "../primitives/Paragraph"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "../primitives/SimpleSelect"; -import { Spinner } from "../primitives/Spinner"; -import { - FilterableEnvironment, - FilterableStatus, - RunListSearchSchema, - environmentKeys, - statusKeys, -} from "./RunStatuses"; -import { TimeFrameFilter } from "./TimeFrameFilter"; -import { Button } from "../primitives/Buttons"; -import { useCallback } from "react"; -import assertNever from "assert-never"; - -export function RunsFilters() { - const navigate = useNavigate(); - const location = useOptimisticLocation(); - const searchParams = new URLSearchParams(location.search); - const { environment, status, from, to } = RunListSearchSchema.parse( - Object.fromEntries(searchParams.entries()) - ); - - const handleFilterChange = useCallback((filterType: string, value: string | undefined) => { - if (value) { - searchParams.set(filterType, value); - } else { - searchParams.delete(filterType); - } - searchParams.delete("cursor"); - searchParams.delete("direction"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); - - const handleStatusChange = useCallback((value: FilterableStatus | "ALL") => { - handleFilterChange("status", value === "ALL" ? undefined : value); - }, []); - - const handleEnvironmentChange = useCallback((value: FilterableEnvironment | "ALL") => { - handleFilterChange("environment", value === "ALL" ? undefined : value); - }, []); - - const handleTimeFrameChange = useCallback((range: { from?: number; to?: number }) => { - if (range.from) { - searchParams.set("from", range.from.toString()); - } else { - searchParams.delete("from"); - } - - if (range.to) { - searchParams.set("to", range.to.toString()); - } else { - searchParams.delete("to"); - } - - searchParams.delete("cursor"); - searchParams.delete("direction"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); - - const clearFilters = useCallback(() => { - searchParams.delete("status"); - searchParams.delete("environment"); - searchParams.delete("from"); - searchParams.delete("to"); - navigate(`${location.pathname}?${searchParams.toString()}`); - }, []); - - return ( -
    - - - - - - - - - - -
    - ); -} - -export function FilterStatusLabel({ status }: { status: FilterableStatus }) { - return {filterStatusTitle(status)}; -} - -export function FilterStatusIcon({ - status, - className, -}: { - status: FilterableStatus; - className: string; -}) { - switch (status) { - case "COMPLETED": - return ; - case "WAITING": - return ; - case "QUEUED": - return ; - case "IN_PROGRESS": - return ; - case "TIMEDOUT": - return ( - - ); - case "CANCELED": - return ; - case "FAILED": - return ; - default: { - assertNever(status); - } - } -} - -export function filterStatusTitle(status: FilterableStatus): string { - switch (status) { - case "QUEUED": - return "Queued"; - case "IN_PROGRESS": - return "In progress"; - case "WAITING": - return "Waiting"; - case "COMPLETED": - return "Completed"; - case "FAILED": - return "Failed"; - case "CANCELED": - return "Canceled"; - case "TIMEDOUT": - return "Timed out"; - default: { - assertNever(status); - } - } -} - -export function filterStatusClassNameColor(status: FilterableStatus): string { - switch (status) { - case "QUEUED": - return "text-charcoal-500"; - case "IN_PROGRESS": - return "text-blue-500"; - case "WAITING": - return "text-blue-500"; - case "COMPLETED": - return "text-green-500"; - case "FAILED": - return "text-rose-500"; - case "CANCELED": - return "text-charcoal-500"; - case "TIMEDOUT": - return "text-amber-300"; - default: { - assertNever(status); - } - } -} diff --git a/apps/webapp/app/components/runs/RunStatuses.tsx b/apps/webapp/app/components/runs/RunStatuses.tsx deleted file mode 100644 index 8ef4d39828..0000000000 --- a/apps/webapp/app/components/runs/RunStatuses.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { NoSymbolIcon } from "@heroicons/react/20/solid"; -import { - CheckCircleIcon, - ClockIcon, - ExclamationTriangleIcon, - PauseCircleIcon, - WrenchIcon, - XCircleIcon, -} from "@heroicons/react/24/solid"; -import type { JobRunStatus } from "@trigger.dev/database"; -import { cn } from "~/utils/cn"; -import { Spinner } from "../primitives/Spinner"; -import { z } from "zod"; -import assertNever from "assert-never"; - -export function RunStatus({ status }: { status: JobRunStatus }) { - return ( - - - - - ); -} - -export function RunStatusLabel({ status }: { status: JobRunStatus }) { - return {runStatusTitle(status)}; -} - -export function RunStatusIcon({ status, className }: { status: JobRunStatus; className: string }) { - switch (status) { - case "SUCCESS": - return ; - case "PENDING": - case "WAITING_TO_CONTINUE": - return ; - case "QUEUED": - case "WAITING_TO_EXECUTE": - return ; - case "PREPROCESSING": - case "STARTED": - case "EXECUTING": - return ; - case "TIMED_OUT": - return ; - case "UNRESOLVED_AUTH": - case "FAILURE": - case "ABORTED": - case "INVALID_PAYLOAD": - return ; - case "WAITING_ON_CONNECTIONS": - return ; - case "CANCELED": - return ; - default: { - assertNever(status); - } - } -} - -export function runStatusTitle(status: JobRunStatus): string { - switch (status) { - case "SUCCESS": - return "Completed"; - case "PENDING": - return "Not started"; - case "STARTED": - return "In progress"; - case "QUEUED": - case "WAITING_TO_EXECUTE": - return "Queued"; - case "EXECUTING": - return "Executing"; - case "WAITING_TO_CONTINUE": - return "Waiting"; - case "FAILURE": - return "Failed"; - case "TIMED_OUT": - return "Timed out"; - case "WAITING_ON_CONNECTIONS": - return "Waiting on connections"; - case "ABORTED": - return "Aborted"; - case "PREPROCESSING": - return "Preprocessing"; - case "CANCELED": - return "Canceled"; - case "UNRESOLVED_AUTH": - return "Unresolved auth"; - case "INVALID_PAYLOAD": - return "Invalid payload"; - default: { - assertNever(status); - } - } -} - -export function runStatusClassNameColor(status: JobRunStatus): string { - switch (status) { - case "SUCCESS": - return "text-green-500"; - case "PENDING": - return "text-charcoal-500"; - case "STARTED": - case "EXECUTING": - case "WAITING_TO_CONTINUE": - case "WAITING_TO_EXECUTE": - return "text-blue-500"; - case "QUEUED": - return "text-charcoal-500"; - case "FAILURE": - case "UNRESOLVED_AUTH": - case "INVALID_PAYLOAD": - return "text-rose-500"; - case "TIMED_OUT": - return "text-amber-300"; - case "WAITING_ON_CONNECTIONS": - return "text-amber-300"; - case "ABORTED": - return "text-rose-500"; - case "PREPROCESSING": - return "text-blue-500"; - case "CANCELED": - return "text-charcoal-500"; - default: { - assertNever(status); - } - } -} - -export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]); -export type Direction = z.infer; - -export const FilterableStatus = z.union([ - z.literal("QUEUED"), - z.literal("IN_PROGRESS"), - z.literal("WAITING"), - z.literal("COMPLETED"), - z.literal("FAILED"), - z.literal("TIMEDOUT"), - z.literal("CANCELED"), -]); -export type FilterableStatus = z.infer; - -export const FilterableEnvironment = z.union([ - z.literal("DEVELOPMENT"), - z.literal("STAGING"), - z.literal("PRODUCTION"), -]); -export type FilterableEnvironment = z.infer; -export const environmentKeys: FilterableEnvironment[] = ["DEVELOPMENT", "STAGING", "PRODUCTION"]; - -export const RunListSearchSchema = z.object({ - cursor: z.string().optional(), - direction: DirectionSchema.optional(), - status: FilterableStatus.optional(), - environment: FilterableEnvironment.optional(), - from: z - .string() - .transform((value) => parseInt(value)) - .optional(), - to: z - .string() - .transform((value) => parseInt(value)) - .optional(), -}); - -export const filterableStatuses: Record = { - QUEUED: ["QUEUED", "WAITING_TO_EXECUTE", "PENDING", "WAITING_ON_CONNECTIONS"], - IN_PROGRESS: ["STARTED", "EXECUTING", "PREPROCESSING"], - WAITING: ["WAITING_TO_CONTINUE"], - COMPLETED: ["SUCCESS"], - FAILED: ["FAILURE", "UNRESOLVED_AUTH", "INVALID_PAYLOAD", "ABORTED"], - TIMEDOUT: ["TIMED_OUT"], - CANCELED: ["CANCELED"], -}; - -export const statusKeys: FilterableStatus[] = Object.keys(filterableStatuses) as FilterableStatus[]; diff --git a/apps/webapp/app/components/runs/RunsTable.tsx b/apps/webapp/app/components/runs/RunsTable.tsx deleted file mode 100644 index c194da9532..0000000000 --- a/apps/webapp/app/components/runs/RunsTable.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { StopIcon } from "@heroicons/react/24/outline"; -import { CheckIcon } from "@heroicons/react/24/solid"; -import { JobRunStatus, RuntimeEnvironmentType, User } from "@trigger.dev/database"; -import { EnvironmentLabel } from "../environments/EnvironmentLabel"; -import { DateTime } from "../primitives/DateTime"; -import { Paragraph } from "../primitives/Paragraph"; -import { Spinner } from "../primitives/Spinner"; -import { - Table, - TableBlankRow, - TableBody, - TableCell, - TableCellChevron, - TableHeader, - TableHeaderCell, - TableRow, -} from "../primitives/Table"; -import { RunStatus } from "./RunStatuses"; -import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3"; - -type RunTableItem = { - id: string; - number: number | null; - environment: { - type: RuntimeEnvironmentType; - userId?: string; - userName?: string; - }; - job: { title: string; slug: string }; - status: JobRunStatus; - startedAt: Date | null; - completedAt: Date | null; - createdAt: Date | null; - executionDuration: number; - version: string; - isTest: boolean; -}; - -type RunsTableProps = { - total: number; - hasFilters: boolean; - showJob?: boolean; - runs: RunTableItem[]; - isLoading?: boolean; - runsParentPath: string; - currentUser: User; -}; - -export function RunsTable({ - total, - hasFilters, - runs, - isLoading = false, - showJob = false, - runsParentPath, - currentUser, -}: RunsTableProps) { - return ( - - - - Run - {showJob && Job} - Env - Status - Started - Duration - Exec Time - Test - Version - Created at - - Go to page - - - - - {total === 0 && !hasFilters ? ( - - {!isLoading && } - - ) : runs.length === 0 ? ( - - {!isLoading && } - - ) : ( - runs.map((run) => { - const path = showJob - ? `${runsParentPath}/jobs/${run.job.slug}/runs/${run.id}/trigger` - : `${runsParentPath}/${run.id}/trigger`; - const usernameForEnv = - currentUser.id !== run.environment.userId ? run.environment.userName : undefined; - return ( - - - {typeof run.number === "number" ? `#${run.number}` : "-"} - - {showJob && {run.job.slug}} - - - - - - - - {run.startedAt ? : "–"} - - - {formatDuration(run.startedAt, run.completedAt, { - style: "short", - })} - - - {formatDurationMilliseconds(run.executionDuration, { - style: "short", - })} - - - {run.isTest ? ( - - ) : ( - - )} - - {run.version} - - {run.createdAt ? : "–"} - - - - ); - }) - )} - {isLoading && ( - - Loading… - - )} - -
    - ); -} - -function NoRuns({ title }: { title: string }) { - return ( -
    - {title} -
    - ); -} diff --git a/apps/webapp/app/components/runs/TimeFrameFilter.tsx b/apps/webapp/app/components/runs/TimeFrameFilter.tsx deleted file mode 100644 index 87e6fe0f95..0000000000 --- a/apps/webapp/app/components/runs/TimeFrameFilter.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { ChevronDownIcon } from "lucide-react"; -import { useCallback, useState } from "react"; -import { cn } from "~/utils/cn"; -import { Button } from "../primitives/Buttons"; -import { ClientTabs, ClientTabsContent, ClientTabsWithUnderline } from "../primitives/ClientTabs"; -import { DateField } from "../primitives/DateField"; -import { formatDateTime } from "../primitives/DateTime"; -import { Paragraph } from "../primitives/Paragraph"; -import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover"; -import { Label } from "../primitives/Label"; - -type RunTimeFrameFilterProps = { - from?: number; - to?: number; - onRangeChanged: (range: { from?: number; to?: number }) => void; -}; - -type Mode = "absolute" | "relative"; - -export function TimeFrameFilter({ from, to, onRangeChanged }: RunTimeFrameFilterProps) { - const [activeTab, setActiveTab] = useState("absolute"); - const [isOpen, setIsOpen] = useState(false); - const [relativeTimeSeconds, setRelativeTimeSeconds] = useState(); - - const fromDate = from ? new Date(from) : undefined; - const toDate = to ? new Date(to) : undefined; - - const relativeTimeFrameChanged = useCallback((value: number) => { - const to = new Date().getTime(); - const from = to - value; - onRangeChanged({ from, to }); - setRelativeTimeSeconds(value); - }, []); - - const absoluteTimeFrameChanged = useCallback(({ from, to }: { from?: Date; to?: Date }) => { - setRelativeTimeSeconds(undefined); - const fromTime = from?.getTime(); - const toTime = to?.getTime(); - onRangeChanged({ from: fromTime, to: toTime }); - }, []); - - return ( - setIsOpen(open)} open={isOpen} modal> - - - - - - setActiveTab(v as Mode)} - className="p-1" - > - - - - - - - - - - - ); -} - -function title( - from: number | undefined, - to: number | undefined, - relativeTimeSeconds: number | undefined -): string { - if (!from && !to) { - return "All time periods"; - } - - if (relativeTimeSeconds !== undefined) { - return timeFrameValues.find((t) => t.value === relativeTimeSeconds)?.label ?? "Timeframe"; - } - - let fromString = from ? formatDateTime(new Date(from), "UTC", ["en-US"], false, true) : undefined; - let toString = to ? formatDateTime(new Date(to), "UTC", ["en-US"], false, true) : undefined; - if (from && !to) { - return `From ${fromString} (UTC)`; - } - - if (!from && to) { - return `To ${toString} (UTC)`; - } - - return `${fromString} - ${toString} (UTC)`; -} - -function RelativeTimeFrame({ - value, - onValueChange, -}: { - value?: number; - onValueChange: (value: number) => void; -}) { - return ( -
    - {timeFrameValues.map((timeframe) => ( - - ))} -
    - ); -} - -const timeFrameValues = [ - { - label: "5 mins", - value: 5 * 60 * 1000, - }, - { - label: "15 mins", - value: 15 * 60 * 1000, - }, - { - label: "30 mins", - value: 30 * 60 * 1000, - }, - { - label: "1 hour", - value: 60 * 60 * 1000, - }, - { - label: "3 hours", - value: 3 * 60 * 60 * 1000, - }, - { - label: "6 hours", - value: 6 * 60 * 60 * 1000, - }, - { - label: "1 day", - value: 24 * 60 * 60 * 1000, - }, - { - label: "3 days", - value: 3 * 24 * 60 * 60 * 1000, - }, - { - label: "7 days", - value: 7 * 24 * 60 * 60 * 1000, - }, - { - label: "10 days", - value: 10 * 24 * 60 * 60 * 1000, - }, - { - label: "14 days", - value: 14 * 24 * 60 * 60 * 1000, - }, - { - label: "30 days", - value: 30 * 24 * 60 * 60 * 1000, - }, -]; - -export type RelativeTimeFrameItem = (typeof timeFrameValues)[number]; - -export function AbsoluteTimeFrame({ - from, - to, - onValueChange, -}: { - from?: Date; - to?: Date; - onValueChange: (value: { from?: Date; to?: Date }) => void; -}) { - return ( -
    -
    -
    - - { - onValueChange({ from: value, to: to }); - }} - granularity="second" - showNowButton - showClearButton - utc - /> -
    -
    - - { - onValueChange({ from: from, to: value }); - }} - granularity="second" - showNowButton - showClearButton - utc - /> -
    -
    -
    - ); -} diff --git a/apps/webapp/app/components/runs/WebhookDeliveryRunsTable.tsx b/apps/webapp/app/components/runs/WebhookDeliveryRunsTable.tsx deleted file mode 100644 index 7b97fcd022..0000000000 --- a/apps/webapp/app/components/runs/WebhookDeliveryRunsTable.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { StopIcon } from "@heroicons/react/24/outline"; -import { CheckIcon } from "@heroicons/react/24/solid"; -import { RuntimeEnvironmentType } from "@trigger.dev/database"; -import { EnvironmentLabel } from "../environments/EnvironmentLabel"; -import { DateTime } from "../primitives/DateTime"; -import { Paragraph } from "../primitives/Paragraph"; -import { Spinner } from "../primitives/Spinner"; -import { - Table, - TableBlankRow, - TableBody, - TableCell, - TableHeader, - TableHeaderCell, - TableRow, -} from "../primitives/Table"; -import { RunStatus } from "./RunStatuses"; -import { formatDuration } from "@trigger.dev/core/v3"; - -type RunTableItem = { - id: string; - number: number; - environment: { - type: RuntimeEnvironmentType; - }; - error: string | null; - createdAt: Date | null; - deliveredAt: Date | null; - verified: boolean; -}; - -type RunsTableProps = { - total: number; - hasFilters: boolean; - runs: RunTableItem[]; - isLoading?: boolean; - runsParentPath: string; -}; - -export function WebhookDeliveryRunsTable({ - total, - hasFilters, - runs, - isLoading = false, - runsParentPath, -}: RunsTableProps) { - return ( - - - - Run - Env - Status - Last Error - Started - Duration - Verified - Created at - - - - {total === 0 && !hasFilters ? ( - - - - ) : runs.length === 0 ? ( - - - - ) : ( - runs.map((run) => { - return ( - - #{run.number} - - - - - - - {run.error?.slice(0, 30) ?? "–"} - {run.createdAt ? : "–"} - - {formatDuration(run.createdAt, run.deliveredAt, { - style: "short", - })} - - - {run.verified ? ( - - ) : ( - - )} - - {run.createdAt ? : "–"} - - ); - }) - )} - {isLoading && ( - - Loading… - - )} - -
    - ); -} -function NoRuns({ title }: { title: string }) { - return ( -
    - {title} -
    - ); -} diff --git a/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx index 2d0439402f..facff746c5 100644 --- a/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx @@ -1,16 +1,11 @@ -import { StopCircleIcon } from "@heroicons/react/20/solid"; import { NoSymbolIcon } from "@heroicons/react/24/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { Form, useFetcher, useNavigation } from "@remix-run/react"; +import { Form, useNavigation } from "@remix-run/react"; import { Button } from "~/components/primitives/Buttons"; -import { - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, -} from "~/components/primitives/Dialog"; +import { DialogContent, DialogHeader } from "~/components/primitives/Dialog"; import { FormButtons } from "~/components/primitives/FormButtons"; import { Paragraph } from "~/components/primitives/Paragraph"; +import { SpinnerWhite } from "~/components/primitives/Spinner"; type CancelRunDialogProps = { runFriendlyId: string; @@ -38,7 +33,7 @@ export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialog name="redirectUrl" value={redirectPath} variant="danger/medium" - LeadingIcon={isLoading ? "spinner-white" : NoSymbolIcon} + LeadingIcon={isLoading ? SpinnerWhite : NoSymbolIcon} disabled={isLoading} shortcut={{ modifiers: ["mod"], key: "enter" }} > diff --git a/apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx b/apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx index 10df63a4de..1f5f07bbf7 100644 --- a/apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx +++ b/apps/webapp/app/components/runs/v3/CheckBatchCompletionDialog.tsx @@ -4,6 +4,7 @@ import { Button } from "~/components/primitives/Buttons"; import { DialogContent, DialogHeader } from "~/components/primitives/Dialog"; import { FormButtons } from "~/components/primitives/FormButtons"; import { Paragraph } from "~/components/primitives/Paragraph"; +import { SpinnerWhite } from "~/components/primitives/Spinner"; type CheckBatchCompletionDialogProps = { batchId: string; @@ -37,7 +38,7 @@ export function CheckBatchCompletionDialog({ name="redirectUrl" value={redirectPath} variant="primary/medium" - LeadingIcon={isLoading ? "spinner-white" : undefined} + LeadingIcon={isLoading ? SpinnerWhite : undefined} disabled={isLoading} shortcut={{ modifiers: ["mod"], key: "enter" }} > diff --git a/apps/webapp/app/components/runs/v3/RetryDeploymentIndexingDialog.tsx b/apps/webapp/app/components/runs/v3/RetryDeploymentIndexingDialog.tsx index 6c1ca5fe92..8eb85f9946 100644 --- a/apps/webapp/app/components/runs/v3/RetryDeploymentIndexingDialog.tsx +++ b/apps/webapp/app/components/runs/v3/RetryDeploymentIndexingDialog.tsx @@ -8,6 +8,7 @@ import { DialogFooter, DialogHeader, } from "~/components/primitives/Dialog"; +import { SpinnerWhite } from "~/components/primitives/Spinner"; type RetryDeploymentIndexingDialogProps = { projectId: string; @@ -46,7 +47,7 @@ export function RetryDeploymentIndexingDialog({ name="redirectUrl" value={redirectPath} variant="primary/medium" - LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon} + LeadingIcon={isLoading ? SpinnerWhite : ArrowPathIcon} disabled={isLoading} shortcut={{ modifiers: ["mod"], key: "enter" }} > diff --git a/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx b/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx index 76ae56e155..50df478098 100644 --- a/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx +++ b/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx @@ -8,6 +8,7 @@ import { DialogFooter, DialogHeader, } from "~/components/primitives/Dialog"; +import { SpinnerWhite } from "~/components/primitives/Spinner"; type RollbackDeploymentDialogProps = { projectId: string; @@ -46,7 +47,7 @@ export function RollbackDeploymentDialog({ name="redirectUrl" value={redirectPath} variant="primary/medium" - LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon} + LeadingIcon={isLoading ? SpinnerWhite : ArrowPathIcon} disabled={isLoading} shortcut={{ modifiers: ["mod"], key: "enter" }} > @@ -88,7 +89,7 @@ export function PromoteDeploymentDialog({ name="redirectUrl" value={redirectPath} variant="primary/medium" - LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon} + LeadingIcon={isLoading ? SpinnerWhite : ArrowPathIcon} disabled={isLoading} shortcut={{ modifiers: ["mod"], key: "enter" }} > diff --git a/apps/webapp/app/components/runs/v3/RunIcon.tsx b/apps/webapp/app/components/runs/v3/RunIcon.tsx index 57f1b05dbe..0e20333c97 100644 --- a/apps/webapp/app/components/runs/v3/RunIcon.tsx +++ b/apps/webapp/app/components/runs/v3/RunIcon.tsx @@ -7,9 +7,10 @@ import { } from "@heroicons/react/20/solid"; import { AttemptIcon } from "~/assets/icons/AttemptIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; -import { TaskCachedIcon } from "~/assets/icons/TaskCachedIcon"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; import { cn } from "~/utils/cn"; +import { tablerIcons } from "~/utils/tablerIcons"; +import tablerSpritePath from "~/components/primitives/tabler-sprite.svg"; +import { TaskCachedIcon } from "~/assets/icons/TaskCachedIcon"; import { PauseIcon } from "~/assets/icons/PauseIcon"; type TaskIconProps = { @@ -29,13 +30,16 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) { const spanNameIcon = spanNameIcons.find(({ matcher }) => matcher.test(spanName)); if (spanNameIcon) { - return ( - } - /> - ); + if (tablerIcons.has("tabler-" + spanNameIcon.iconName)) { + return ; + } else if ( + spanNameIcon.iconName.startsWith("tabler-") && + tablerIcons.has(spanNameIcon.iconName) + ) { + return ; + } + + ; } if (!name) return ; @@ -68,11 +72,13 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) { return ; } + return ; +} + +function TablerIcon({ name, className }: { name: string; className?: string }) { return ( - } - /> + + + ); } diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index 7cbb182e76..4e686bc7b6 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -1,4 +1,4 @@ -import { XMarkIcon } from "@heroicons/react/20/solid"; +import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { useNavigate } from "@remix-run/react"; import { type RuntimeEnvironment } from "@trigger.dev/database"; import { useCallback } from "react"; @@ -97,7 +97,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul { process.exit(1); }); -const sqsEventConsumer = singleton("sqsEventConsumer", getSharedSqsEventConsumer); - singleton("RunEngineEventBusHandlers", registerRunEngineEventBusHandlers); export { apiRateLimiter } from "./services/apiRateLimit.server"; diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 45118358c3..84e3c52b91 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -42,7 +42,6 @@ const EnvironmentSchema = z.object({ TELEMETRY_TRIGGER_API_KEY: z.string().optional(), TELEMETRY_TRIGGER_API_URL: z.string().optional(), TRIGGER_TELEMETRY_DISABLED: z.string().optional(), - HIGHLIGHT_PROJECT_ID: z.string().optional(), AUTH_GITHUB_CLIENT_ID: z.string().optional(), AUTH_GITHUB_CLIENT_SECRET: z.string().optional(), EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(), diff --git a/apps/webapp/app/hooks/useFilterJobs.ts b/apps/webapp/app/hooks/useFilterJobs.ts deleted file mode 100644 index 9d220952e8..0000000000 --- a/apps/webapp/app/hooks/useFilterJobs.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ProjectJob } from "~/presenters/JobListPresenter.server"; -import { useTextFilter } from "./useTextFilter"; -import { useToggleFilter } from "./useToggleFilter"; - -export function useFilterJobs(jobs: ProjectJob[], onlyActiveJobs = false) { - const toggleFilterRes = useToggleFilter({ - items: jobs, - filter: (job, onlyActiveJobs) => { - if (onlyActiveJobs && job.status !== "ACTIVE") { - return false; - } - return true; - }, - defaultValue: onlyActiveJobs, - }); - - const textFilterRes = useTextFilter({ - items: toggleFilterRes.filteredItems, - filter: (job, text) => { - if (job.slug.toLowerCase().includes(text.toLowerCase())) return true; - if (job.title.toLowerCase().includes(text.toLowerCase())) return true; - if (job.event.title.toLowerCase().includes(text.toLowerCase())) return true; - if ( - job.integrations.some((integration) => - integration.title.toLowerCase().includes(text.toLowerCase()) - ) - ) - return true; - if ( - job.properties && - job.properties.some((property) => property.text.toLowerCase().includes(text.toLowerCase())) - ) - return true; - - return false; - }, - }); - - return { - filteredItems: textFilterRes.filteredItems, - filterText: textFilterRes.filterText, - setFilterText: textFilterRes.setFilterText, - onlyActiveJobs: toggleFilterRes.isToggleActive, - setOnlyActiveJobs: toggleFilterRes.setToggleActive, - }; -} diff --git a/apps/webapp/app/hooks/useHighlight.ts b/apps/webapp/app/hooks/useHighlight.ts deleted file mode 100644 index 99a796101b..0000000000 --- a/apps/webapp/app/hooks/useHighlight.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { H } from "highlight.run"; -import { useUserChanged } from "./useUser"; - -export function useHighlight() { - useUserChanged((user) => { - if (!user) { - return; - } - - H.identify(user.id, { - email: user.email, - }); - }); -} diff --git a/apps/webapp/app/hooks/useIntegrationClient.tsx b/apps/webapp/app/hooks/useIntegrationClient.tsx deleted file mode 100644 index bfe9fad244..0000000000 --- a/apps/webapp/app/hooks/useIntegrationClient.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { UIMatch } from "@remix-run/react"; -import { UseDataFunctionReturn } from "remix-typedjson"; -import invariant from "tiny-invariant"; -import type { loader } from "~/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route"; -import { useTypedMatchesData } from "./useTypedMatchData"; - -export type MatchedClient = UseDataFunctionReturn["client"]; - -export function useOptionalIntegrationClient(matches?: UIMatch[]) { - const routeMatch = useTypedMatchesData({ - id: "routes/_app.orgs.$organizationSlug.integrations_.$clientParam", - matches, - }); - - return routeMatch?.client; -} - -export function useIntegrationClient(matches?: UIMatch[]) { - const integration = useOptionalIntegrationClient(matches); - invariant(integration, "Integration must be defined"); - return integration; -} diff --git a/apps/webapp/app/hooks/useJob.tsx b/apps/webapp/app/hooks/useJob.tsx deleted file mode 100644 index 7064e8f30e..0000000000 --- a/apps/webapp/app/hooks/useJob.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { UseDataFunctionReturn } from "remix-typedjson"; -import invariant from "tiny-invariant"; -import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route"; -import { useChanged } from "./useChanged"; -import { UIMatch } from "@remix-run/react"; -import { useTypedMatchesData } from "./useTypedMatchData"; - -export type MatchedJob = UseDataFunctionReturn["job"]; - -export const jobMatchId = - "routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam"; - -export function useOptionalJob(matches?: UIMatch[]) { - const routeMatch = useTypedMatchesData({ - id: jobMatchId, - matches, - }); - - if (!routeMatch) { - return undefined; - } - - return routeMatch.job; -} - -export function useJob(matches?: UIMatch[]) { - const job = useOptionalJob(matches); - invariant(job, "Job must be defined"); - return job; -} - -export const useJobChanged = (action: (org: MatchedJob | undefined) => void) => { - useChanged(useOptionalJob, action); -}; diff --git a/apps/webapp/app/hooks/usePostHog.ts b/apps/webapp/app/hooks/usePostHog.ts index 81c73baabf..99e2597dfe 100644 --- a/apps/webapp/app/hooks/usePostHog.ts +++ b/apps/webapp/app/hooks/usePostHog.ts @@ -4,7 +4,6 @@ import { useEffect, useRef } from "react"; import { useOrganizationChanged } from "./useOrganizations"; import { useOptionalUser, useUserChanged } from "./useUser"; import { useProjectChanged } from "./useProject"; -import { useJobChanged } from "./useJob"; export const usePostHog = (apiKey?: string, logging = false, debug = false): void => { const postHogInitialized = useRef(false); @@ -63,14 +62,6 @@ export const usePostHog = (apiKey?: string, logging = false, debug = false): voi } }); - 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; diff --git a/apps/webapp/app/hooks/useProjectSetupComplete.ts b/apps/webapp/app/hooks/useProjectSetupComplete.ts deleted file mode 100644 index cbfb78b5cd..0000000000 --- a/apps/webapp/app/hooks/useProjectSetupComplete.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useEffect } from "react"; -import { projectPath, projectStreamingPath } from "~/utils/pathBuilder"; -import { useProject } from "./useProject"; -import { useOrganization } from "./useOrganizations"; -import { useNavigate } from "@remix-run/react"; -import { useEventSource } from "./useEventSource"; - -export function useProjectSetupComplete() { - const project = useProject(); - const organization = useOrganization(); - const navigate = useNavigate(); - const events = useEventSource(projectStreamingPath(project.id), { - event: "message", - }); - - useEffect(() => { - if (events !== null) { - // This uses https://www.npmjs.com/package/canvas-confetti - if ("confetti" in window && typeof window.confetti !== "undefined") { - const duration = 3.5 * 1000; - const animationEnd = Date.now() + duration; - const defaults = { - startVelocity: 30, - spread: 360, - ticks: 60, - zIndex: 0, - colors: [ - "#E7FF52", - "#41FF54", - "rgb(245 158 11)", - "rgb(22 163 74)", - "rgb(37 99 235)", - "rgb(67 56 202)", - "rgb(219 39 119)", - "rgb(225 29 72)", - "rgb(217 70 239)", - ], - }; - function randomInRange(min: number, max: number): number { - return Math.random() * (max - min) + min; - } - // @ts-ignore - const interval = setInterval(function () { - const timeLeft = animationEnd - Date.now(); - - if (timeLeft <= 0) { - return clearInterval(interval); - } - - const particleCount = 60 * (timeLeft / duration); - // since particles fall down, start a bit higher than random - // @ts-ignore - window.confetti( - Object.assign({}, defaults, { - particleCount, - origin: { x: randomInRange(0.1, 0.4), y: Math.random() - 0.2 }, - }) - ); - // @ts-ignore - window.confetti( - Object.assign({}, defaults, { - particleCount, - origin: { x: randomInRange(0.6, 0.9), y: Math.random() - 0.2 }, - }) - ); - }, 250); - } - - navigate(projectPath(organization, project)); - } - // WARNING Don't put the revalidator in the useEffect deps array or bad things will happen - }, [events]); // eslint-disable-line react-hooks/exhaustive-deps -} diff --git a/apps/webapp/app/hooks/useRun.ts b/apps/webapp/app/hooks/useRun.ts deleted file mode 100644 index 40c2516c96..0000000000 --- a/apps/webapp/app/hooks/useRun.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { UIMatch } from "@remix-run/react"; -import { UseDataFunctionReturn } from "remix-typedjson"; -import invariant from "tiny-invariant"; -import type { loader as runLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route"; -import { useOptionalProject } from "./useProject"; -import { useTypedMatchesData } from "./useTypedMatchData"; - -export type MatchedRun = UseDataFunctionReturn["run"]; - -export function useOptionalRun(matches?: UIMatch[]) { - const project = useOptionalProject(matches); - const routeMatch = useTypedMatchesData({ - id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam", - matches, - }); - - if (!project || !routeMatch || !routeMatch.run) { - return undefined; - } - - return routeMatch.run; -} - -export function useRun(matches?: UIMatch[]) { - const run = useOptionalRun(matches); - invariant(run, "Run must be present"); - return run; -} diff --git a/apps/webapp/app/models/endpoint.server.ts b/apps/webapp/app/models/endpoint.server.ts deleted file mode 100644 index 56f4925855..0000000000 --- a/apps/webapp/app/models/endpoint.server.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { VERCEL_RESPONSE_TIMEOUT_STATUS_CODES } from "~/consts"; -import { prisma } from "~/db.server"; -import { Prettify } from "~/lib.es5"; - -export type ExtendedEndpoint = Prettify>>; - -export async function findEndpoint(id: string) { - return await prisma.endpoint.findUniqueOrThrow({ - where: { - id, - }, - include: { - environment: { - include: { - project: true, - organization: true, - }, - }, - }, - }); -} - -export function detectResponseIsTimeout(rawBody: string, response?: Response) { - if (!response) { - return false; - } - - return ( - isResponseVercelTimeout(response) || - isResponseCloudfrontTimeout(response) || - isResponseDenoDeployTimeout(rawBody, response) || - isResponseCloudflareTimeout(rawBody, response) - ); -} - -function isResponseCloudflareTimeout(rawBody: string, response: Response) { - return ( - response.status === 503 && - rawBody.includes("Worker exceeded resource limits") && - typeof response.headers.get("cf-ray") === "string" - ); -} - -function isResponseVercelTimeout(response: Response) { - return ( - VERCEL_RESPONSE_TIMEOUT_STATUS_CODES.includes(response.status) || - response.headers.get("x-vercel-error") === "FUNCTION_INVOCATION_TIMEOUT" - ); -} - -function isResponseDenoDeployTimeout(rawBody: string, response: Response) { - return response.status === 502 && rawBody.includes("TIME_LIMIT"); -} - -function isResponseCloudfrontTimeout(response: Response) { - return response.status === 504 && typeof response.headers.get("x-amz-cf-id") === "string"; -} diff --git a/apps/webapp/app/models/eventDispatcher.server.ts b/apps/webapp/app/models/eventDispatcher.server.ts deleted file mode 100644 index 70de2ac6af..0000000000 --- a/apps/webapp/app/models/eventDispatcher.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from "zod"; - -export const JobVersionDispatchableSchema = z.object({ - type: z.literal("JOB_VERSION"), - id: z.string(), -}); - -export const DynamicTriggerDispatchableSchema = z.object({ - type: z.literal("DYNAMIC_TRIGGER"), - id: z.string(), -}); - -export const EphemeralDispatchableSchema = z.object({ - type: z.literal("EPHEMERAL"), - url: z.string(), -}); - -export const DispatchableSchema = z.discriminatedUnion("type", [ - JobVersionDispatchableSchema, - DynamicTriggerDispatchableSchema, - EphemeralDispatchableSchema, -]); diff --git a/apps/webapp/app/models/job.server.ts b/apps/webapp/app/models/job.server.ts deleted file mode 100644 index 66cc310ec4..0000000000 --- a/apps/webapp/app/models/job.server.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { prisma } from "~/db.server"; -export type { Job, JobRunStatus } from "@trigger.dev/database"; - -export function findJobByParams({ - userId, - slug, - projectSlug, - organizationSlug, -}: { - userId: string; - slug: string; - projectSlug: string; - organizationSlug: string; -}) { - //just the very basic info because we already fetched it for the Jobs list - return prisma.job.findFirst({ - select: { id: true, title: true }, - where: { - slug, - project: { slug: projectSlug }, - organization: { slug: organizationSlug, members: { some: { userId } } }, - }, - }); -} diff --git a/apps/webapp/app/models/jobRun.server.ts b/apps/webapp/app/models/jobRun.server.ts deleted file mode 100644 index 6b5ea0bc90..0000000000 --- a/apps/webapp/app/models/jobRun.server.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { JobRun, JobRunStatus } from "@trigger.dev/database"; - -const COMPLETED_STATUSES: Array = [ - "CANCELED", - "ABORTED", - "SUCCESS", - "TIMED_OUT", - "INVALID_PAYLOAD", - "FAILURE", - "UNRESOLVED_AUTH", -]; - -export function isRunCompleted(status: JobRunStatus) { - return COMPLETED_STATUSES.includes(status); -} - -export type RunBasicStatus = "WAITING" | "PENDING" | "RUNNING" | "COMPLETED" | "FAILED"; - -export function runBasicStatus(status: JobRunStatus): RunBasicStatus { - switch (status) { - case "WAITING_ON_CONNECTIONS": - case "QUEUED": - case "PREPROCESSING": - case "PENDING": - return "PENDING"; - case "STARTED": - case "EXECUTING": - case "WAITING_TO_CONTINUE": - case "WAITING_TO_EXECUTE": - return "RUNNING"; - case "FAILURE": - case "TIMED_OUT": - case "UNRESOLVED_AUTH": - case "CANCELED": - case "ABORTED": - case "INVALID_PAYLOAD": - return "FAILED"; - case "SUCCESS": - return "COMPLETED"; - default: { - const _exhaustiveCheck: never = status; - throw new Error(`Non-exhaustive match for value: ${status}`); - } - } -} - -export function runOriginalStatus(status: JobRunStatus) { - switch (status) { - case "EXECUTING": - case "WAITING_TO_CONTINUE": - case "WAITING_TO_EXECUTE": - return "STARTED"; - default: - return status; - } -} diff --git a/apps/webapp/app/models/runConnection.server.ts b/apps/webapp/app/models/runConnection.server.ts deleted file mode 100644 index 732781ecc7..0000000000 --- a/apps/webapp/app/models/runConnection.server.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { Integration, RunConnection } from "@trigger.dev/database"; -import type { ConnectionAuth } from "@trigger.dev/core"; -import type { ConnectionWithSecretReference } from "~/services/externalApis/integrationAuthRepository.server"; -import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server"; - -export type ResolvableRunConnection = RunConnection & { - integration: Integration; - connection: ConnectionWithSecretReference | null; -}; - -export async function resolveRunConnections( - connections: Array -): Promise<{ auth: Record; success: boolean }> { - let allResolved = true; - - const result: Record = {}; - - for (const connection of connections) { - if (connection.integration.authSource !== "HOSTED") { - continue; - } - - const auth = await resolveRunConnection(connection); - - if (!auth) { - allResolved = false; - continue; - } - - result[connection.key] = auth; - } - - return { auth: result, success: allResolved }; -} - -export async function resolveRunConnection( - connection: ResolvableRunConnection -): Promise { - if (!connection.connection) { - return; - } - - const response = await integrationAuthRepository.getCredentials(connection.connection); - - if (!response) { - return; - } - - return { - type: "oauth2", - scopes: response.scopes, - accessToken: response.accessToken, - }; -} - -export async function resolveApiConnection( - connection?: ConnectionWithSecretReference -): Promise { - if (!connection) { - return; - } - - const response = await integrationAuthRepository.getCredentials(connection); - - if (!response) { - return; - } - - return { - type: "oauth2", - scopes: response.scopes, - accessToken: response.accessToken, - }; -} diff --git a/apps/webapp/app/models/sourceConnection.server.ts b/apps/webapp/app/models/sourceConnection.server.ts deleted file mode 100644 index 8ca6aec116..0000000000 --- a/apps/webapp/app/models/sourceConnection.server.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ExternalAccount, Integration, TriggerSource } from "@trigger.dev/database"; -import { ConnectionAuth } from "@trigger.dev/core"; -import { PrismaClientOrTransaction } from "~/db.server"; -import { integrationAuthRepository } from "~/services/externalApis/integrationAuthRepository.server"; -import { logger } from "~/services/logger.server"; - -type ResolvableTriggerSource = TriggerSource & { - integration: Integration; - externalAccount: ExternalAccount | null; -}; - -export async function resolveSourceConnection( - tx: PrismaClientOrTransaction, - source: ResolvableTriggerSource -): Promise { - if (source.integration.authSource !== "HOSTED") return; - - const connection = await getConnection(tx, source); - - if (!connection) { - logger.error( - `Integration connection not found for source ${source.id}, integration ${source.integration.id}` - ); - return; - } - - const response = await integrationAuthRepository.getCredentials(connection); - - if (!response) { - return; - } - - return { - type: "oauth2", - scopes: response.scopes, - accessToken: response.accessToken, - }; -} - -function getConnection(tx: PrismaClientOrTransaction, source: ResolvableTriggerSource) { - if (source.externalAccount) { - return tx.integrationConnection.findFirst({ - where: { - integrationId: source.integration.id, - externalAccountId: source.externalAccount.id, - }, - include: { - dataReference: true, - }, - }); - } - - return tx.integrationConnection.findFirst({ - where: { - integrationId: source.integration.id, - }, - include: { - dataReference: true, - }, - }); -} diff --git a/apps/webapp/app/models/task.server.ts b/apps/webapp/app/models/task.server.ts index 079dd66bdc..0d0791ac7e 100644 --- a/apps/webapp/app/models/task.server.ts +++ b/apps/webapp/app/models/task.server.ts @@ -1,123 +1,6 @@ -import type { JobRun, Task, TaskAttempt, TaskTriggerSource } from "@trigger.dev/database"; -import { CachedTask, ServerTask } from "@trigger.dev/core"; +import type { TaskTriggerSource } from "@trigger.dev/database"; import { PrismaClientOrTransaction, sqlDatabaseSchema } from "~/db.server"; -export type TaskWithAttempts = Task & { - attempts: TaskAttempt[]; - run: { forceYieldImmediately: boolean }; -}; - -export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask { - return { - id: task.id, - name: task.name, - icon: task.icon, - noop: task.noop, - startedAt: task.startedAt, - completedAt: task.completedAt, - delayUntil: task.delayUntil, - status: task.status, - description: task.description, - params: task.params as any, - output: task.outputIsUndefined ? undefined : (task.output as any), - context: task.context as any, - properties: task.properties as any, - style: task.style as any, - error: task.error, - parentId: task.parentId, - attempts: task.attempts.length, - idempotencyKey: task.idempotencyKey, - operation: task.operation, - callbackUrl: task.callbackUrl, - forceYield: task.run.forceYieldImmediately, - childExecutionMode: task.childExecutionMode, - }; -} - -export type TaskForCaching = Pick< - Task, - "id" | "status" | "idempotencyKey" | "noop" | "output" | "parentId" | "outputIsUndefined" ->; - -export function prepareTasksForCaching( - possibleTasks: TaskForCaching[], - maxSize: number -): { - tasks: CachedTask[]; - cursor: string | undefined; -} { - const tasks = possibleTasks.filter((task) => task.status === "COMPLETED" && !task.noop); - - // Select tasks using greedy approach - const tasksToRun: CachedTask[] = []; - let remainingSize = maxSize; - - for (const task of tasks) { - const cachedTask = prepareTaskForCaching(task); - const size = calculateCachedTaskSize(cachedTask); - - if (size <= remainingSize) { - tasksToRun.push(cachedTask); - remainingSize -= size; - } - } - - return { - tasks: tasksToRun, - cursor: tasks.length > tasksToRun.length ? tasks[tasksToRun.length].id : undefined, - }; -} - -export function prepareTasksForCachingLegacy( - possibleTasks: TaskForCaching[], - maxSize: number -): { - tasks: CachedTask[]; - cursor: string | undefined; -} { - const tasks = possibleTasks.filter((task) => task.status === "COMPLETED"); - - // Prepare tasks and calculate their sizes - const availableTasks = tasks.map((task) => { - const cachedTask = prepareTaskForCaching(task); - return { task: cachedTask, size: calculateCachedTaskSize(cachedTask) }; - }); - - // Sort tasks in ascending order by size - availableTasks.sort((a, b) => a.size - b.size); - - // Select tasks using greedy approach - const tasksToRun: CachedTask[] = []; - let remainingSize = maxSize; - - for (const { task, size } of availableTasks) { - if (size <= remainingSize) { - tasksToRun.push(task); - remainingSize -= size; - } - } - - return { - tasks: tasksToRun, - cursor: undefined, - }; -} - -function prepareTaskForCaching(task: TaskForCaching): CachedTask { - return { - id: task.idempotencyKey, // We should eventually move this back to task.id - status: task.status, - idempotencyKey: task.idempotencyKey, - noop: task.noop, - output: task.outputIsUndefined ? undefined : (task.output as any), - parentId: task.parentId, - }; -} - -function calculateCachedTaskSize(task: CachedTask): number { - return JSON.stringify(task).length; -} - /** * * @param prisma An efficient query to get all task identifiers for a project. diff --git a/apps/webapp/app/presenters/ApiRunPresenter.server.ts b/apps/webapp/app/presenters/ApiRunPresenter.server.ts deleted file mode 100644 index a3d18f9e2b..0000000000 --- a/apps/webapp/app/presenters/ApiRunPresenter.server.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Job } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; - -type ApiRunOptions = { - runId: Job["id"]; - maxTasks?: number; - taskDetails?: boolean; - subTasks?: boolean; - cursor?: string; -}; - -export class ApiRunPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - runId, - maxTasks = 20, - taskDetails = false, - subTasks = false, - cursor, - }: ApiRunOptions) { - const take = Math.min(maxTasks, 50); - - return await prisma.jobRun.findFirst({ - where: { - id: runId, - }, - select: { - id: true, - status: true, - startedAt: true, - updatedAt: true, - completedAt: true, - environmentId: true, - output: true, - tasks: { - select: { - id: true, - parentId: true, - displayKey: true, - status: true, - name: true, - icon: true, - startedAt: true, - completedAt: true, - params: taskDetails, - output: taskDetails, - }, - where: { - parentId: subTasks ? undefined : null, - }, - orderBy: { - id: "asc", - }, - take: take + 1, - cursor: cursor - ? { - id: cursor, - } - : undefined, - }, - statuses: { - select: { key: true, label: true, state: true, data: true, history: true }, - }, - }, - }); - } -} diff --git a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts b/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts deleted file mode 100644 index 50a22425e8..0000000000 --- a/apps/webapp/app/presenters/EnvironmentsPresenter.server.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { PrismaClient, prisma } from "~/db.server"; -import { Project } from "~/models/project.server"; -import { User } from "~/models/user.server"; -import type { - Endpoint, - EndpointIndex, - EndpointIndexStatus, - RuntimeEnvironment, - RuntimeEnvironmentType, -} from "@trigger.dev/database"; -import { - EndpointIndexError, - EndpointIndexErrorSchema, - IndexEndpointStats, - parseEndpointIndexStats, -} from "@trigger.dev/core"; -import { sortEnvironments } from "~/utils/environmentSort"; - -export type Client = { - slug: string; - endpoints: { - DEVELOPMENT: ClientEndpoint; - PRODUCTION: ClientEndpoint; - STAGING?: ClientEndpoint; - }; -}; - -export type ClientEndpoint = - | { - state: "unconfigured"; - environment: { - id: string; - apiKey: string; - type: RuntimeEnvironmentType; - }; - } - | { - state: "configured"; - id: string; - slug: string; - url: string | null; - indexWebhookPath: string; - latestIndex?: { - status: EndpointIndexStatus; - source: string; - updatedAt: Date; - stats?: IndexEndpointStats; - error?: EndpointIndexError; - }; - environment: { - id: string; - apiKey: string; - type: RuntimeEnvironmentType; - }; - }; - -export class EnvironmentsPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - baseUrl, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - baseUrl: string; - }) { - const environments = await this.#prismaClient.runtimeEnvironment.findMany({ - select: { - id: true, - apiKey: true, - pkApiKey: true, - type: true, - slug: true, - orgMember: { - select: { - userId: true, - }, - }, - endpoints: { - select: { - id: true, - slug: true, - url: true, - indexingHookIdentifier: true, - indexings: { - select: { - status: true, - source: true, - updatedAt: true, - stats: true, - error: true, - }, - take: 1, - orderBy: { - updatedAt: "desc", - }, - }, - }, - where: { - url: { - not: null, - }, - }, - }, - }, - where: { - project: { - slug: projectSlug, - }, - organization: { - members: { - some: { - userId, - }, - }, - }, - }, - }); - - //filter out environments the only development ones belong to the current user - const filtered = environments.filter((environment) => { - if (environment.type === "DEVELOPMENT") { - return environment.orgMember?.userId === userId; - } - return true; - }); - - //get all the possible client slugs - const clientSlugs = new Set(); - for (const environment of filtered) { - for (const endpoint of environment.endpoints) { - clientSlugs.add(endpoint.slug); - } - } - - //build up list of clients for display, with endpoints by type - const clients: Client[] = []; - for (const slug of clientSlugs) { - const developmentEnvironment = filtered.find( - (environment) => environment.type === "DEVELOPMENT" - ); - if (!developmentEnvironment) { - throw new Error("Development environment not found, this should not happen"); - } - - const stagingEnvironment = filtered.find((environment) => environment.type === "STAGING"); - - const productionEnvironment = filtered.find( - (environment) => environment.type === "PRODUCTION" - ); - if (!productionEnvironment) { - throw new Error("Production environment not found, this should not happen"); - } - - const client: Client = { - slug, - endpoints: { - DEVELOPMENT: { - state: "unconfigured", - environment: developmentEnvironment, - }, - PRODUCTION: { - state: "unconfigured", - environment: productionEnvironment, - }, - STAGING: stagingEnvironment - ? { state: "unconfigured", environment: stagingEnvironment } - : undefined, - }, - }; - - const devEndpoint = developmentEnvironment.endpoints.find( - (endpoint) => endpoint.slug === slug - ); - if (devEndpoint) { - client.endpoints.DEVELOPMENT = endpointClient(devEndpoint, developmentEnvironment, baseUrl); - } - - if (stagingEnvironment) { - const stagingEndpoint = stagingEnvironment.endpoints.find( - (endpoint) => endpoint.slug === slug - ); - - if (stagingEndpoint) { - client.endpoints.STAGING = endpointClient(stagingEndpoint, stagingEnvironment, baseUrl); - } - } - - const prodEndpoint = productionEnvironment.endpoints.find( - (endpoint) => endpoint.slug === slug - ); - if (prodEndpoint) { - client.endpoints.PRODUCTION = endpointClient(prodEndpoint, productionEnvironment, baseUrl); - } - - clients.push(client); - } - - return { - environments: sortEnvironments( - filtered.map((environment) => ({ - id: environment.id, - apiKey: environment.apiKey, - pkApiKey: environment.pkApiKey, - type: environment.type, - slug: environment.slug, - })) - ), - clients, - }; - } -} - -function endpointClient( - endpoint: Pick & { - indexings: Pick[]; - }, - environment: Pick, - baseUrl: string -): ClientEndpoint { - return { - state: "configured" as const, - id: endpoint.id, - slug: endpoint.slug, - url: endpoint.url, - indexWebhookPath: `${baseUrl}/api/v1/endpoints/${environment.id}/${endpoint.slug}/index/${endpoint.indexingHookIdentifier}`, - latestIndex: endpoint.indexings[0] - ? { - status: endpoint.indexings[0].status, - source: endpoint.indexings[0].source, - updatedAt: endpoint.indexings[0].updatedAt, - stats: parseEndpointIndexStats(endpoint.indexings[0].stats), - error: endpoint.indexings[0].error - ? EndpointIndexErrorSchema.parse(endpoint.indexings[0].error) - : undefined, - } - : undefined, - environment: environment, - }; -} diff --git a/apps/webapp/app/presenters/EnvironmentsStreamPresenter.server.ts b/apps/webapp/app/presenters/EnvironmentsStreamPresenter.server.ts deleted file mode 100644 index 3d7bf8877c..0000000000 --- a/apps/webapp/app/presenters/EnvironmentsStreamPresenter.server.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { PrismaClient, prisma } from "~/db.server"; -import { Project } from "~/models/project.server"; -import { User } from "~/models/user.server"; -import { sse } from "~/utils/sse.server"; - -type EnvironmentSignalsMap = { - [x: string]: { - lastUpdatedAt: number; - lastTotalEndpointUpdatedTime: number; - lastTotalIndexingUpdatedTime: number; - }; -}; - -export class EnvironmentsStreamPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - request, - userId, - projectSlug, - }: { - request: Request; - userId: User["id"]; - projectSlug: Project["slug"]; - }) { - let lastEnvironmentSignals: EnvironmentSignalsMap; - - return sse({ - request, - run: async (send, stop) => { - const nextEnvironmentSignals = await this.#runForUpdates({ - userId, - projectSlug, - }); - - if (!nextEnvironmentSignals) { - return stop(); - } - - const lastEnvironmentIds = lastEnvironmentSignals - ? Object.keys(lastEnvironmentSignals) - : []; - const nextEnvironmentIds = Object.keys(nextEnvironmentSignals); - - if ( - //push update if the number of environments is different - nextEnvironmentIds.length !== lastEnvironmentIds.length || - //push update if the list of ids is different - lastEnvironmentIds.some((id) => !nextEnvironmentSignals[id]) || - nextEnvironmentIds.some((id) => !lastEnvironmentSignals[id]) || - //push update if any signals changed - nextEnvironmentIds.some( - (id) => - nextEnvironmentSignals[id].lastUpdatedAt !== - lastEnvironmentSignals[id].lastUpdatedAt || - nextEnvironmentSignals[id].lastTotalEndpointUpdatedTime !== - lastEnvironmentSignals[id].lastTotalEndpointUpdatedTime || - nextEnvironmentSignals[id].lastTotalIndexingUpdatedTime !== - lastEnvironmentSignals[id].lastTotalIndexingUpdatedTime - ) - ) { - send({ data: new Date().toISOString() }); - } - - lastEnvironmentSignals = nextEnvironmentSignals; - }, - }); - } - - async #runForUpdates({ - userId, - projectSlug, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - }) { - const environments = await this.#prismaClient.runtimeEnvironment.findMany({ - select: { - id: true, - updatedAt: true, - endpoints: { - select: { - updatedAt: true, - indexings: { - select: { - updatedAt: true, - }, - }, - }, - }, - }, - where: { - project: { - slug: projectSlug, - }, - organization: { - members: { - some: { - userId, - }, - }, - }, - }, - }); - - if (!environments) return null; - - const environmentSignalsMap = environments.reduce((acc, environment) => { - const lastUpdatedAt = environment.updatedAt.getTime(); - const lastTotalEndpointUpdatedTime = environment.endpoints.reduce( - (prev, endpoint) => prev + endpoint.updatedAt.getTime(), - 0 - ); - const lastTotalIndexingUpdatedTime = environment.endpoints.reduce( - (prev, endpoint) => - prev + - endpoint.indexings.reduce((prev, indexing) => prev + indexing.updatedAt.getTime(), 0), - 0 - ); - - return { - ...acc, - [environment.id]: { - lastUpdatedAt, - lastTotalEndpointUpdatedTime, - lastTotalIndexingUpdatedTime, - }, - }; - }, {}); - - return environmentSignalsMap; - } -} diff --git a/apps/webapp/app/presenters/EventPresenter.server.ts b/apps/webapp/app/presenters/EventPresenter.server.ts deleted file mode 100644 index 8c569ceca0..0000000000 --- a/apps/webapp/app/presenters/EventPresenter.server.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { PrismaClient, prisma } from "~/db.server"; - -export type Event = NonNullable>>; - -export class EventPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - eventId, - }: { - userId: string; - projectSlug: string; - organizationSlug: string; - eventId: string; - }) { - // Find the organization that the user is a member of - const organization = await this.#prismaClient.organization.findFirstOrThrow({ - where: { - slug: organizationSlug, - members: { some: { userId } }, - }, - }); - - // Find the project scoped to the organization - const project = await this.#prismaClient.project.findFirstOrThrow({ - where: { - slug: projectSlug, - organizationId: organization.id, - }, - }); - - const event = await this.#prismaClient.eventRecord.findFirst({ - select: { - id: true, - name: true, - payload: true, - context: true, - timestamp: true, - deliveredAt: true, - }, - where: { - id: eventId, - projectId: project.id, - organizationId: organization.id, - }, - }); - - if (!event) { - throw new Error("Could not find Event"); - } - - return { - id: event.id, - name: event.name, - timestamp: event.timestamp, - payload: JSON.stringify(event.payload, null, 2), - context: JSON.stringify(event.context, null, 2), - deliveredAt: event.deliveredAt, - }; - } -} diff --git a/apps/webapp/app/presenters/HttpEndpointPresenter.server.ts b/apps/webapp/app/presenters/HttpEndpointPresenter.server.ts deleted file mode 100644 index 242d6478a9..0000000000 --- a/apps/webapp/app/presenters/HttpEndpointPresenter.server.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { z } from "zod"; -import { PrismaClient, prisma } from "~/db.server"; -import { sortEnvironments } from "~/utils/environmentSort"; -import { httpEndpointUrl } from "~/services/httpendpoint/HandleHttpEndpointService.server"; -import { getSecretStore } from "~/services/secrets/secretStore.server"; -import { projectPath } from "~/utils/pathBuilder"; - -export class HttpEndpointPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - httpEndpointKey, - }: { - userId: string; - projectSlug: string; - organizationSlug: string; - httpEndpointKey: string; - }) { - const httpEndpoint = await this.#prismaClient.triggerHttpEndpoint.findFirst({ - select: { - id: true, - key: true, - icon: true, - title: true, - updatedAt: true, - projectId: true, - secretReference: { - select: { - key: true, - provider: true, - }, - }, - httpEndpointEnvironments: { - select: { - id: true, - immediateResponseFilter: true, - skipTriggeringRuns: true, - source: true, - active: true, - updatedAt: true, - environment: { - select: { - type: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - }, - }, - }, - webhook: { - select: { - id: true, - key: true, - }, - }, - }, - where: { - key: httpEndpointKey, - project: { - slug: projectSlug, - organization: { - members: { - some: { - userId, - }, - }, - }, - }, - }, - }); - - if (!httpEndpoint) { - throw new Error("Could not find http endpoint"); - } - - const environments = await this.#prismaClient.runtimeEnvironment.findMany({ - select: { - id: true, - type: true, - slug: true, - shortcode: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - where: { - projectId: httpEndpoint.projectId, - }, - }); - - const relevantEnvironments = sortEnvironments( - environments - .filter( - (environment) => environment.orgMember === null || environment.orgMember.userId === userId - ) - .map((environment) => ({ - ...environment, - webhookUrl: httpEndpointUrl({ httpEndpointId: httpEndpoint.id, environment }), - })) - ); - - //get the secret - const secretStore = getSecretStore(httpEndpoint.secretReference.provider); - let secret: string | undefined; - try { - const secretData = await secretStore.getSecretOrThrow( - z.object({ secret: z.string() }), - httpEndpoint.secretReference.key - ); - secret = secretData.secret; - } catch (e) { - let error = e instanceof Error ? e.message : JSON.stringify(e); - throw new Error(`Could not retrieve secret: ${error}`); - } - if (!secret) { - throw new Error("Could not find secret"); - } - - const httpEndpointEnvironments = httpEndpoint.httpEndpointEnvironments - .filter( - (httpEndpointEnvironment) => - httpEndpointEnvironment.environment.orgMember === null || - httpEndpointEnvironment.environment.orgMember.userId === userId - ) - .map((endpointEnv) => ({ - ...endpointEnv, - immediateResponseFilter: endpointEnv.immediateResponseFilter != null, - environment: { - type: endpointEnv.environment.type, - }, - webhookUrl: relevantEnvironments.find((e) => e.type === endpointEnv.environment.type) - ?.webhookUrl, - })); - - const projectRootPath = projectPath({ slug: organizationSlug }, { slug: projectSlug }); - - return { - httpEndpoint: { - ...httpEndpoint, - httpEndpointEnvironments, - webhookLink: httpEndpoint.webhook - ? `${projectRootPath}/triggers/webhooks/${httpEndpoint.webhook.id}` - : undefined, - }, - environments: relevantEnvironments, - unconfiguredEnvironments: relevantEnvironments.filter( - (e) => httpEndpointEnvironments.find((h) => h.environment.type === e.type) === undefined - ), - secret, - }; - } -} diff --git a/apps/webapp/app/presenters/HttpEndpointsPresenter.server.ts b/apps/webapp/app/presenters/HttpEndpointsPresenter.server.ts deleted file mode 100644 index 8f4d3134f9..0000000000 --- a/apps/webapp/app/presenters/HttpEndpointsPresenter.server.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { PrismaClient, prisma } from "~/db.server"; -import { Project } from "~/models/project.server"; -import { User } from "~/models/user.server"; - -export class HttpEndpointsPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - slug, - }: Pick & { - userId: User["id"]; - }) { - const httpEndpoints = await this.#prismaClient.triggerHttpEndpoint.findMany({ - select: { - id: true, - key: true, - icon: true, - title: true, - updatedAt: true, - httpEndpointEnvironments: { - select: { - id: true, - environment: { - select: { - type: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - }, - }, - }, - }, - where: { - project: { - slug, - organization: { - members: { - some: { - userId, - }, - }, - }, - }, - }, - }); - - return httpEndpoints.map((httpEndpoint) => ({ - ...httpEndpoint, - httpEndpointEnvironments: httpEndpoint.httpEndpointEnvironments.filter( - (httpEndpointEnvironment) => - httpEndpointEnvironment.environment.orgMember === null || - httpEndpointEnvironment.environment.orgMember.userId === userId - ), - })); - } -} diff --git a/apps/webapp/app/presenters/IntegrationClientConnectionsPresenter.server.ts b/apps/webapp/app/presenters/IntegrationClientConnectionsPresenter.server.ts deleted file mode 100644 index 3e02167ec7..0000000000 --- a/apps/webapp/app/presenters/IntegrationClientConnectionsPresenter.server.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { User } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { ConnectionMetadataSchema } from "~/services/externalApis/types"; - -export class IntegrationClientConnectionsPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - organizationSlug, - clientSlug, - }: { - userId: User["id"]; - organizationSlug: Organization["slug"]; - clientSlug: string; - }) { - const connections = await this.#prismaClient.integrationConnection.findMany({ - select: { - id: true, - expiresAt: true, - metadata: true, - connectionType: true, - createdAt: true, - updatedAt: true, - _count: { - select: { - runConnections: true, - }, - }, - }, - where: { - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, - integration: { - slug: clientSlug, - }, - }, - orderBy: { - createdAt: "desc", - }, - }); - - return { - connections: connections.map((c) => ({ - id: c.id, - expiresAt: c.expiresAt, - metadata: c.metadata != null ? ConnectionMetadataSchema.parse(c.metadata) : null, - type: c.connectionType, - createdAt: c.createdAt, - updatedAt: c.updatedAt, - runCount: c._count.runConnections, - })), - }; - } -} diff --git a/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts b/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts deleted file mode 100644 index ae88353fec..0000000000 --- a/apps/webapp/app/presenters/IntegrationClientPresenter.server.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { User } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { env } from "~/env.server"; -import { Organization } from "~/models/organization.server"; -import { HelpSchema, OAuthClientSchema } from "~/services/externalApis/types"; -import { getSecretStore } from "~/services/secrets/secretStore.server"; - -export class IntegrationClientPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - organizationSlug, - clientSlug, - }: { - userId: User["id"]; - organizationSlug: Organization["slug"]; - clientSlug: string; - }) { - const integration = await this.#prismaClient.integration.findFirst({ - select: { - id: true, - title: true, - slug: true, - authMethod: { - select: { - key: true, - type: true, - name: true, - help: true, - }, - }, - authSource: true, - definition: { - select: { - id: true, - name: true, - packageName: true, - icon: true, - }, - }, - connectionType: true, - customClientReference: { - select: { - key: true, - }, - }, - createdAt: true, - _count: { - select: { - jobIntegrations: { - where: { - job: { - organization: { - slug: organizationSlug, - }, - internal: false, - deletedAt: null, - }, - }, - }, - }, - }, - }, - where: { - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, - slug: clientSlug, - }, - }); - - if (!integration) { - return undefined; - } - - const secretStore = getSecretStore(env.SECRET_STORE, { - prismaClient: this.#prismaClient, - }); - - let clientId: String | undefined = undefined; - if (integration.customClientReference) { - const clientConfig = await secretStore.getSecret( - OAuthClientSchema, - integration.customClientReference.key - ); - clientId = clientConfig?.id; - } - - const help = integration.authMethod?.help - ? HelpSchema.parse(integration.authMethod?.help) - : undefined; - - return { - id: integration.id, - title: integration.title ?? integration.slug, - slug: integration.slug, - integrationIdentifier: integration.definition.id, - jobCount: integration._count.jobIntegrations, - createdAt: integration.createdAt, - customClientId: clientId, - type: integration.connectionType, - integration: { - identifier: integration.definition.id, - name: integration.definition.name, - packageName: integration.definition.packageName, - icon: integration.definition.icon, - }, - authMethod: { - type: - integration.authMethod?.type ?? - (integration.authSource === "RESOLVER" ? "resolver" : "local"), - name: - integration.authMethod?.name ?? - (integration.authSource === "RESOLVER" ? "Auth Resolver" : "Local Auth"), - }, - help, - }; - } -} diff --git a/apps/webapp/app/presenters/IntegrationClientScopesPresenter.server.ts b/apps/webapp/app/presenters/IntegrationClientScopesPresenter.server.ts deleted file mode 100644 index 25bbc783a0..0000000000 --- a/apps/webapp/app/presenters/IntegrationClientScopesPresenter.server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { User } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; - -import { Scope } from "~/services/externalApis/types"; - -export class IntegrationClientScopesPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - organizationSlug, - clientSlug, - }: { - userId: User["id"]; - organizationSlug: Organization["slug"]; - clientSlug: string; - }) { - const integration = await this.#prismaClient.integration.findFirst({ - select: { - authMethod: true, - scopes: true, - }, - where: { - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, - slug: clientSlug, - }, - }); - - if (!integration) { - throw new Error("Client not found"); - } - - const authMethodScopes = (integration.authMethod?.scopes ?? []) as Scope[]; - - return { - scopes: integration.scopes.map((s) => { - const matchingScope = authMethodScopes.find((scope) => scope.name === s); - - return { - name: s, - description: matchingScope?.description, - }; - }), - }; - } -} diff --git a/apps/webapp/app/presenters/IntegrationsPresenter.server.ts b/apps/webapp/app/presenters/IntegrationsPresenter.server.ts deleted file mode 100644 index 5b3c003fbc..0000000000 --- a/apps/webapp/app/presenters/IntegrationsPresenter.server.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { User } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { env } from "~/env.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { Api, apisList } from "~/services/externalApis/apis.server"; -import { integrationCatalog } from "~/services/externalApis/integrationCatalog.server"; -import { Integration, OAuthClientSchema } from "~/services/externalApis/types"; -import { getSecretStore } from "~/services/secrets/secretStore.server"; - -export type IntegrationOrApi = - | ({ - type: "integration"; - } & Integration) - | ({ type: "api" } & Api & { voted: boolean }); - -export type Client = Awaited>["clients"][number]; - -export class IntegrationsPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - organizationSlug, - }: { - userId: User["id"]; - organizationSlug: Organization["slug"]; - }) { - const clients = await this.#prismaClient.integration.findMany({ - select: { - id: true, - title: true, - slug: true, - description: true, - setupStatus: true, - authMethod: { - select: { - type: true, - name: true, - }, - }, - definition: { - select: { - id: true, - name: true, - icon: true, - }, - }, - authSource: true, - connectionType: true, - scopes: true, - customClientReference: { - select: { - key: true, - }, - }, - createdAt: true, - _count: { - select: { - connections: true, - jobIntegrations: { - where: { - job: { - organization: { - slug: organizationSlug, - }, - internal: false, - deletedAt: null, - }, - }, - }, - }, - }, - }, - where: { - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, - }, - orderBy: { - title: "asc", - }, - }); - - const secretStore = getSecretStore(env.SECRET_STORE, { - prismaClient: this.#prismaClient, - }); - - const enrichedClients = await Promise.all( - clients.map(async (c) => { - let clientId: String | undefined = undefined; - if (c.customClientReference) { - const clientConfig = await secretStore.getSecret( - OAuthClientSchema, - c.customClientReference.key - ); - clientId = clientConfig?.id; - } - - return { - id: c.id, - title: c.title ?? c.slug, - icon: c.definition.icon ?? c.definition.id, - slug: c.slug, - integrationIdentifier: c.definition.id, - description: c.description, - scopesCount: c.scopes.length, - connectionsCount: c._count.connections, - jobCount: c._count.jobIntegrations, - createdAt: c.createdAt, - customClientId: clientId, - integration: { - identifier: c.definition.id, - name: c.definition.name, - }, - authMethod: { - type: c.authMethod?.type ?? (c.authSource === "RESOLVER" ? "resolver" : "local"), - name: - c.authMethod?.name ?? (c.authSource === "RESOLVER" ? "Auth Resolver" : "Local Only"), - }, - authSource: c.authSource, - setupStatus: c.setupStatus, - }; - }) - ); - - const setupClients = enrichedClients.filter((c) => c.setupStatus === "COMPLETE"); - const clientMissingFields = enrichedClients.filter((c) => c.setupStatus === "MISSING_FIELDS"); - - const integrations = Object.values(integrationCatalog.getIntegrations()).map((i) => ({ - type: "integration" as const, - ...i, - })); - - //get all apis, some don't have integrations yet. - //get whether the user has voted for them or not - const votes = await this.#prismaClient.apiIntegrationVote.findMany({ - select: { - apiIdentifier: true, - }, - where: { - userId, - }, - }); - - const apis = apisList - .filter((a) => !integrations.some((i) => i.identifier === a.identifier)) - .map((a) => ({ - type: "api" as const, - ...a, - voted: votes.some((v) => v.apiIdentifier === a.identifier), - })); - - const options = [...integrations, ...apis].sort((a, b) => a.name.localeCompare(b.name)); - - return { - clients: setupClients, - clientMissingFields, - options, - callbackUrl: `${env.APP_ORIGIN}/oauth2/callback`, - }; - } -} diff --git a/apps/webapp/app/presenters/JobListPresenter.server.ts b/apps/webapp/app/presenters/JobListPresenter.server.ts deleted file mode 100644 index 0edbe39346..0000000000 --- a/apps/webapp/app/presenters/JobListPresenter.server.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { - DisplayProperty, - DisplayPropertySchema, - EventSpecificationSchema, -} from "@trigger.dev/core"; -import { PrismaClient, Prisma, prisma, sqlDatabaseSchema } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { User } from "~/models/user.server"; -import { z } from "zod"; -import { projectPath } from "~/utils/pathBuilder"; -import { JobRunStatus } from "@trigger.dev/database"; -import { BasePresenter } from "./v3/basePresenter.server"; - -export type ProjectJob = Awaited>[0]; - -export class JobListPresenter extends BasePresenter { - - - public async call({ - userId, - projectSlug, - organizationSlug, - integrationSlug, - }: { - userId: User["id"]; - projectSlug?: Project["slug"]; - organizationSlug: Organization["slug"]; - integrationSlug?: string; - }) { - const orgWhere: Prisma.JobWhereInput["organization"] = organizationSlug - ? { slug: organizationSlug, members: { some: { userId } } } - : { members: { some: { userId } } }; - - const integrationsWhere: Prisma.JobWhereInput["integrations"] = integrationSlug - ? { some: { integration: { slug: integrationSlug } } } - : {}; - - const jobs = await this._replica.job.findMany({ - select: { - id: true, - slug: true, - title: true, - integrations: { - select: { - key: true, - integration: { - select: { - slug: true, - definition: true, - setupStatus: true, - }, - }, - }, - }, - versions: { - select: { - version: true, - eventSpecification: true, - properties: true, - status: true, - triggerLink: true, - triggerHelp: true, - environment: { - select: { - type: true, - }, - }, - }, - orderBy: [{ updatedAt: "desc" }], - take: 1, - }, - dynamicTriggers: { - select: { - type: true, - }, - }, - project: { - select: { - slug: true, - }, - }, - }, - where: { - internal: false, - deletedAt: null, - organization: orgWhere, - project: projectSlug - ? { - slug: projectSlug, - } - : undefined, - integrations: integrationsWhere, - }, - orderBy: [{ title: "asc" }], - }); - - let latestRuns = [] as { - createdAt: Date; - status: JobRunStatus; - jobId: string; - rn: BigInt; - }[]; - - if (jobs.length > 0) { - latestRuns = await this._replica.$queryRaw< - { - createdAt: Date; - status: JobRunStatus; - jobId: string; - rn: BigInt; - }[] - >` - SELECT * FROM ( - SELECT - "id", - "createdAt", - "status", - "jobId", - ROW_NUMBER() OVER(PARTITION BY "jobId" ORDER BY "createdAt" DESC) as rn - FROM - ${sqlDatabaseSchema}."JobRun" - WHERE - "jobId" IN (${Prisma.join(jobs.map((j) => j.id))}) - ) t - WHERE rn = 1;`; - } - - return jobs - .flatMap((job) => { - const version = job.versions.at(0); - if (!version) { - return []; - } - - const eventSpecification = EventSpecificationSchema.parse(version.eventSpecification); - - const integrations = job.integrations.map((integration) => ({ - key: integration.key, - title: integration.integration.slug, - icon: integration.integration.definition.icon ?? integration.integration.definition.id, - setupStatus: integration.integration.setupStatus, - })); - - //deduplicate integrations - const uniqueIntegrations = new Map(); - integrations.forEach((i) => { - uniqueIntegrations.set(i.key, i); - }); - - let properties: DisplayProperty[] = []; - - if (eventSpecification.properties) { - properties = [...properties, ...eventSpecification.properties]; - } - - if (version.properties) { - const versionProperties = z.array(DisplayPropertySchema).parse(version.properties); - properties = [...properties, ...versionProperties]; - } - - const latestRun = latestRuns.find((r) => r.jobId === job.id); - - return [ - { - id: job.id, - slug: job.slug, - title: job.title, - version: version.version, - status: version.status, - dynamic: job.dynamicTriggers.length > 0, - event: { - title: eventSpecification.title, - icon: eventSpecification.icon, - source: eventSpecification.source, - link: projectSlug - ? `${projectPath({ slug: organizationSlug }, { slug: projectSlug })}/${ - version.triggerLink - }` - : undefined, - }, - integrations: Array.from(uniqueIntegrations.values()), - hasIntegrationsRequiringAction: integrations.some( - (i) => i.setupStatus === "MISSING_FIELDS" - ), - environment: version.environment, - lastRun: latestRun, - properties, - projectSlug: job.project.slug, - }, - ]; - }) - .filter(Boolean); - } -} diff --git a/apps/webapp/app/presenters/JobPresenter.server.ts b/apps/webapp/app/presenters/JobPresenter.server.ts deleted file mode 100644 index 6db5ca3044..0000000000 --- a/apps/webapp/app/presenters/JobPresenter.server.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { - DisplayProperty, - DisplayPropertySchema, - EventSpecificationSchema, - TriggerHelpSchema, -} from "@trigger.dev/core"; -import { PrismaClient, Prisma, prisma } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { User } from "~/models/user.server"; -import { z } from "zod"; -import { projectPath } from "~/utils/pathBuilder"; -import { Job } from "@trigger.dev/database"; -import { BasePresenter } from "./v3/basePresenter.server"; - -export class JobPresenter extends BasePresenter { - public async call({ - userId, - jobSlug, - projectSlug, - organizationSlug, - }: { - userId: User["id"]; - jobSlug: Job["slug"]; - projectSlug: Project["slug"]; - organizationSlug: Organization["slug"]; - }) { - const job = await this._replica.job.findFirst({ - select: { - id: true, - slug: true, - title: true, - aliases: { - select: { - version: { - select: { - version: true, - eventSpecification: true, - properties: true, - status: true, - concurrencyLimit: true, - concurrencyLimitGroup: { - select: { - name: true, - concurrencyLimit: true, - }, - }, - runs: { - select: { - createdAt: true, - status: true, - }, - take: 1, - orderBy: [{ createdAt: "desc" }], - }, - integrations: { - select: { - key: true, - integration: { - select: { - slug: true, - definition: true, - setupStatus: true, - }, - }, - }, - }, - triggerLink: true, - triggerHelp: true, - }, - }, - environment: { - select: { - type: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - }, - }, - where: { - name: "latest", - }, - }, - dynamicTriggers: { - select: { - type: true, - }, - }, - project: { - select: { - slug: true, - }, - }, - }, - where: { - slug: jobSlug, - deletedAt: null, - organization: { - members: { - some: { - userId, - }, - }, - }, - project: { - slug: projectSlug, - }, - }, - }); - - if (!job) { - return undefined; - } - - //the best alias to select: - // 1. Logged-in user dev - // 2. Prod - // 3. Any other user's dev - const sortedAliases = job.aliases.sort((a, b) => { - if (a.environment.type === "DEVELOPMENT" && a.environment.orgMember?.userId === userId) { - return -1; - } - - if (b.environment.type === "DEVELOPMENT" && b.environment.orgMember?.userId === userId) { - return 1; - } - - if (a.environment.type === "PRODUCTION") { - return -1; - } - - if (b.environment.type === "PRODUCTION") { - return 1; - } - - return 0; - }); - - const alias = sortedAliases.at(0); - - if (!alias) { - throw new Error(`No aliases found for job ${job.id}, this should never happen.`); - } - - const eventSpecification = EventSpecificationSchema.parse(alias.version.eventSpecification); - - const lastRuns = job.aliases - .map((alias) => alias.version.runs.at(0)) - .filter(Boolean) - .sort((a, b) => { - return b.createdAt.getTime() - a.createdAt.getTime(); - }); - - const lastRun = lastRuns.at(0); - - const integrations = alias.version.integrations.map((integration) => ({ - key: integration.key, - title: integration.integration.slug, - icon: integration.integration.definition.icon ?? integration.integration.definition.id, - setupStatus: integration.integration.setupStatus, - })); - - let properties: DisplayProperty[] = []; - - if (eventSpecification.properties) { - properties = [...properties, ...eventSpecification.properties]; - } - - if (alias.version.properties) { - const versionProperties = z.array(DisplayPropertySchema).parse(alias.version.properties); - properties = [...properties, ...versionProperties]; - } - - const environments = job.aliases.map((alias) => ({ - type: alias.environment.type, - enabled: alias.version.status === "ACTIVE", - lastRun: alias.version.runs.at(0)?.createdAt, - version: alias.version.version, - concurrencyLimit: alias.version.concurrencyLimit, - concurrencyLimitGroup: alias.version.concurrencyLimitGroup, - })); - - const projectRootPath = projectPath({ slug: organizationSlug }, { slug: projectSlug }); - - return { - id: job.id, - slug: job.slug, - title: job.title, - version: alias.version.version, - status: alias.version.status, - dynamic: job.dynamicTriggers.length > 0, - event: { - title: eventSpecification.title, - icon: eventSpecification.icon, - source: eventSpecification.source, - link: alias.version.triggerLink - ? `${projectRootPath}/${alias.version.triggerLink}` - : undefined, - }, - integrations, - hasIntegrationsRequiringAction: integrations.some((i) => i.setupStatus === "MISSING_FIELDS"), - lastRun, - properties, - environments, - }; - } -} diff --git a/apps/webapp/app/presenters/OrgBillingPlanPresenter.ts b/apps/webapp/app/presenters/OrgBillingPlanPresenter.ts deleted file mode 100644 index 7b3132cdb3..0000000000 --- a/apps/webapp/app/presenters/OrgBillingPlanPresenter.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BillingService } from "../services/billing.v2.server"; -import { BasePresenter } from "./v3/basePresenter.server"; - -export class OrgBillingPlanPresenter extends BasePresenter { - public async call({ slug, isManagedCloud }: { slug: string; isManagedCloud: boolean }) { - const billingPresenter = new BillingService(isManagedCloud); - const plans = await billingPresenter.getPlans(); - - if (plans === undefined) { - return; - } - - const organization = await this._replica.organization.findFirst({ - where: { - slug, - }, - }); - - if (!organization) { - return; - } - - const maxConcurrency = await this._replica.$queryRaw< - { organization_id: string; max_concurrent_runs: BigInt }[] - >`WITH events AS ( - SELECT - re.event_time, - re.organization_id, - re.event_type, - SUM(re.event_type) OVER (PARTITION BY re.organization_id ORDER BY re.event_time) AS running_total - FROM - triggerdotdev_events.run_executions re - WHERE - re.organization_id = ${organization.id} - AND re.event_time >= DATE_TRUNC('month', - CURRENT_DATE) - ) - SELECT - organization_id, MAX(running_total) AS max_concurrent_runs - FROM - events - GROUP BY - organization_id;`; - - return { - plans, - maxConcurrency: - maxConcurrency.at(0) !== undefined - ? Number(maxConcurrency[0].max_concurrent_runs) - : undefined, - }; - } -} diff --git a/apps/webapp/app/presenters/OrgUsagePresenter.server.ts b/apps/webapp/app/presenters/OrgUsagePresenter.server.ts deleted file mode 100644 index 4c64c520eb..0000000000 --- a/apps/webapp/app/presenters/OrgUsagePresenter.server.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { estimate } from "@trigger.dev/platform/v2"; -import { sqlDatabaseSchema } from "~/db.server"; -import { featuresForRequest } from "~/features.server"; -import { BillingService } from "~/services/billing.v2.server"; -import { BasePresenter } from "./v3/basePresenter.server"; - -export class OrgUsagePresenter extends BasePresenter { - public async call({ userId, slug, request }: { userId: string; slug: string; request: Request }) { - const organization = await this._replica.organization.findFirst({ - where: { - slug, - members: { - some: { - userId, - }, - }, - }, - }); - - if (!organization) { - throw new Error("Organization not found"); - } - - // Get count of runs since the start of the current month - const runsCount = await this._replica.jobRun.count({ - where: { - organizationId: organization.id, - createdAt: { - gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1), - }, - internal: false, - }, - }); - - // Get the count of the runs for the last 6 months, by month. So for example we want the data shape to be: - // [ - // { month: "2021-01", count: 10 }, - // { month: "2021-02", count: 20 }, - // { month: "2021-03", count: 30 }, - // { month: "2021-04", count: 40 }, - // { month: "2021-05", count: 50 }, - // { month: "2021-06", count: 60 }, - // ] - // This will be used to generate the chart on the usage page - // Use prisma queryRaw for this since prisma doesn't support grouping by month - const monthlyRunsDataRaw = await this._replica.$queryRaw< - { - month: string; - count: number; - }[] - >`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM ${sqlDatabaseSchema}."JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' AND "internal" = FALSE GROUP BY month ORDER BY month ASC`; - - const hasMonthlyRunData = monthlyRunsDataRaw.length > 0; - const monthlyRunsData = monthlyRunsDataRaw.map((obj) => ({ - name: obj.month, - total: Number(obj.count), // Convert BigInt to Number - })); - - const monthlyRunsDataDisplay = fillInMissingRunMonthlyData(monthlyRunsData, 6); - - // Max concurrency each day over past 30 days - const concurrencyChartRawData = await this._replica.$queryRaw< - { day: Date; max_concurrent_runs: BigInt }[] - >` - WITH time_boundaries AS ( - SELECT generate_series( - NOW() - interval '30 days', - NOW(), - interval '1 day' - ) AS day_start - ), - events AS ( - SELECT - day_start, - event_time, - event_type, - SUM(event_type) OVER (ORDER BY event_time) AS running_total - FROM - time_boundaries - JOIN - triggerdotdev_events.run_executions - ON - event_time >= day_start AND event_time < day_start + interval '1 day' - WHERE triggerdotdev_events.run_executions.organization_id = ${organization.id} - ), - max_concurrent_per_day AS ( - SELECT - date_trunc('day', event_time) AS day, - MAX(running_total) AS max_concurrent_runs - FROM - events - GROUP BY day - ) - SELECT - day, - max_concurrent_runs - FROM - max_concurrent_per_day - ORDER BY - day;`; - - const ThirtyDaysAgo = new Date(); - ThirtyDaysAgo.setDate(ThirtyDaysAgo.getDate() - 30); - ThirtyDaysAgo.setUTCHours(0, 0, 0, 0); - - const hasConcurrencyData = concurrencyChartRawData.length > 0; - const concurrencyChartRawDataFilledIn = fillInMissingConcurrencyDays( - ThirtyDaysAgo, - 31, - concurrencyChartRawData - ); - - const dailyRunsRawData = await this._replica.$queryRaw< - { day: Date; runs: BigInt }[] - >`SELECT date_trunc('day', "createdAt") as day, COUNT(*) as runs FROM ${sqlDatabaseSchema}."JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '30 days' AND "internal" = FALSE GROUP BY day`; - - const hasDailyRunsData = dailyRunsRawData.length > 0; - const dailyRunsDataFilledIn = fillInMissingDailyRuns(ThirtyDaysAgo, 31, dailyRunsRawData); - - const endOfMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1); - endOfMonth.setDate(endOfMonth.getDate() - 1); - const projectedRunsCount = Math.round( - runsCount / (new Date().getDate() / endOfMonth.getDate()) - ); - - const { isManagedCloud } = featuresForRequest(request); - const billingPresenter = new BillingService(isManagedCloud); - const plans = await billingPresenter.getPlans(); - - let runCostEstimation: number | undefined = undefined; - let projectedRunCostEstimation: number | undefined = undefined; - - if (plans) { - const estimationResult = estimate({ - usage: { runs: runsCount }, - plans: [plans.free, plans.paid], - }); - runCostEstimation = estimationResult?.cost.runsCost; - - const projectedEstimationResult = estimate({ - usage: { runs: projectedRunsCount }, - plans: [plans.free, plans.paid], - }); - projectedRunCostEstimation = projectedEstimationResult?.cost.runsCost; - } - - const periodStart = new Date(); - periodStart.setDate(1); - periodStart.setUTCHours(0, 0, 0, 0); - - const periodEnd = new Date(); - periodEnd.setDate(1); - periodEnd.setMonth(periodEnd.getMonth() + 1); - periodEnd.setUTCHours(0, 0, 0, 0); - - return { - id: organization.id, - runsCount, - projectedRunsCount, - monthlyRunsData: monthlyRunsDataDisplay, - hasMonthlyRunData, - concurrencyData: concurrencyChartRawDataFilledIn, - hasConcurrencyData, - dailyRunsData: dailyRunsDataFilledIn, - hasDailyRunsData, - runCostEstimation, - projectedRunCostEstimation, - periodStart, - periodEnd, - }; - } -} - -// This will fill in missing chart data with zeros -// So for example, if data is [{ name: "2021-01", total: 10 }, { name: "2021-03", total: 30 }] and the totalNumberOfMonths is 6 -// And the current month is "2021-04", then this function will return: -// [{ name: "2020-11", total: 0 }, { name: "2020-12", total: 0 }, { name: "2021-01", total: 10 }, { name: "2021-02", total: 0 }, { name: "2021-03", total: 30 }, { name: "2021-04", total: 0 }] -function fillInMissingRunMonthlyData( - data: Array<{ name: string; total: number }>, - totalNumberOfMonths: number -): Array<{ name: string; total: number }> { - const currentMonth = new Date().toISOString().slice(0, 7); - - const startMonth = new Date( - new Date(currentMonth).getFullYear(), - new Date(currentMonth).getMonth() - (totalNumberOfMonths - 2), - 1 - ) - .toISOString() - .slice(0, 7); - - const months = getMonthsBetween(startMonth, currentMonth); - - let completeData = months.map((month) => { - let foundData = data.find((d) => d.name === month); - return foundData ? { ...foundData } : { name: month, total: 0 }; - }); - - return completeData; -} - -function fillInMissingConcurrencyDays( - startDate: Date, - days: number, - data: Array<{ day: Date; max_concurrent_runs: BigInt }> -) { - const outputData: Array<{ date: Date; maxConcurrentRuns: number }> = []; - for (let i = 0; i < days; i++) { - const date = new Date(startDate); - date.setDate(date.getDate() + i); - - const foundData = data.find((d) => d.day.toISOString() === date.toISOString()); - if (!foundData) { - outputData.push({ - date, - maxConcurrentRuns: 0, - }); - } else { - outputData.push({ - date, - maxConcurrentRuns: Number(foundData.max_concurrent_runs), - }); - } - } - - return outputData; -} - -function fillInMissingDailyRuns( - startDate: Date, - days: number, - data: Array<{ day: Date; runs: BigInt }> -) { - const outputData: Array<{ date: Date; runs: number }> = []; - for (let i = 0; i < days; i++) { - const date = new Date(startDate); - date.setDate(date.getDate() + i); - - const foundData = data.find((d) => d.day.toISOString() === date.toISOString()); - if (!foundData) { - outputData.push({ - date, - runs: 0, - }); - } else { - outputData.push({ - date, - runs: Number(foundData.runs), - }); - } - } - - return outputData; -} - -// Start month will be like 2023-03 and endMonth will be like 2023-10 -// The result should be an array of months between these two months, including the start and end month -// So for example, if startMonth is 2023-03 and endMonth is 2023-10, the result should be: -// ["2023-03", "2023-04", "2023-05", "2023-06", "2023-07", "2023-08", "2023-09", "2023-10"] -function getMonthsBetween(startMonth: string, endMonth: string): string[] { - // Initialize result array - const result: string[] = []; - - // Parse the year and month from startMonth and endMonth - let [startYear, startMonthNum] = startMonth.split("-").map(Number); - let [endYear, endMonthNum] = endMonth.split("-").map(Number); - - // Loop through each month between startMonth and endMonth - for (let year = startYear; year <= endYear; year++) { - let monthStart = year === startYear ? startMonthNum : 1; - let monthEnd = year === endYear ? endMonthNum : 12; - - for (let month = monthStart; month <= monthEnd; month++) { - // Format the month into a string and add it to the result array - result.push(`${year}-${String(month).padStart(2, "0")}`); - } - } - - return result; -} diff --git a/apps/webapp/app/presenters/RunListPresenter.server.ts b/apps/webapp/app/presenters/RunListPresenter.server.ts deleted file mode 100644 index 91c1e5a98f..0000000000 --- a/apps/webapp/app/presenters/RunListPresenter.server.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { - Direction, - FilterableEnvironment, - FilterableStatus, - filterableStatuses, -} from "~/components/runs/RunStatuses"; -import { getUsername } from "~/utils/username"; -import { BasePresenter } from "./v3/basePresenter.server"; - -type RunListOptions = { - userId: string; - eventId?: string; - jobSlug?: string; - organizationSlug: string; - projectSlug: string; - direction?: Direction; - filterStatus?: FilterableStatus; - filterEnvironment?: FilterableEnvironment; - cursor?: string; - pageSize?: number; - from?: number; - to?: number; -}; - -const DEFAULT_PAGE_SIZE = 20; - -export type RunList = Awaited>; - -export class RunListPresenter extends BasePresenter { - public async call({ - userId, - eventId, - jobSlug, - organizationSlug, - projectSlug, - filterEnvironment, - filterStatus, - direction = "forward", - cursor, - pageSize = DEFAULT_PAGE_SIZE, - from, - to, - }: RunListOptions) { - const filterStatuses = filterStatus ? filterableStatuses[filterStatus] : undefined; - - const directionMultiplier = direction === "forward" ? 1 : -1; - - // Find the organization that the user is a member of - const organization = await this._replica.organization.findFirstOrThrow({ - select: { - id: true, - }, - where: { - slug: organizationSlug, - members: { some: { userId } }, - }, - }); - - // Find the project scoped to the organization - const project = await this._replica.project.findFirstOrThrow({ - select: { - id: true, - }, - where: { - slug: projectSlug, - organizationId: organization.id, - }, - }); - - const job = jobSlug - ? await this._replica.job.findFirstOrThrow({ - where: { - slug: jobSlug, - projectId: project.id, - }, - }) - : undefined; - - const event = eventId - ? await this._replica.eventRecord.findUnique({ where: { id: eventId } }) - : undefined; - - const runs = await this._replica.jobRun.findMany({ - select: { - id: true, - number: true, - startedAt: true, - completedAt: true, - createdAt: true, - executionDuration: true, - isTest: true, - status: true, - environment: { - select: { - type: true, - slug: true, - orgMember: { - select: { - user: { - select: { - id: true, - name: true, - displayName: true, - }, - }, - }, - }, - }, - }, - version: { - select: { - version: true, - }, - }, - job: { - select: { - slug: true, - title: true, - }, - }, - }, - where: { - eventId: event?.id, - jobId: job?.id, - projectId: project.id, - organizationId: organization.id, - status: filterStatuses ? { in: filterStatuses } : undefined, - environment: filterEnvironment ? { type: filterEnvironment } : undefined, - startedAt: { - gte: from ? new Date(from).toISOString() : undefined, - lte: to ? new Date(to).toISOString() : undefined, - }, - }, - orderBy: [{ id: "desc" }], - //take an extra record to tell if there are more - take: directionMultiplier * (pageSize + 1), - //skip the cursor if there is one - skip: cursor ? 1 : 0, - cursor: cursor - ? { - id: cursor, - } - : undefined, - }); - - const hasMore = runs.length > pageSize; - - //get cursors for next and previous pages - let next: string | undefined; - let previous: string | undefined; - switch (direction) { - case "forward": - previous = cursor ? runs.at(0)?.id : undefined; - if (hasMore) { - next = runs[pageSize - 1]?.id; - } - break; - case "backward": - if (hasMore) { - previous = runs[1]?.id; - next = runs[pageSize]?.id; - } else { - next = runs[pageSize - 1]?.id; - } - break; - } - - const runsToReturn = - direction === "backward" && hasMore ? runs.slice(1, pageSize + 1) : runs.slice(0, pageSize); - - return { - runs: runsToReturn.map((run) => ({ - id: run.id, - number: run.number, - startedAt: run.startedAt, - completedAt: run.completedAt, - createdAt: run.createdAt, - executionDuration: run.executionDuration, - isTest: run.isTest, - status: run.status, - version: run.version?.version ?? "unknown", - environment: { - type: run.environment.type, - slug: run.environment.slug, - userId: run.environment.orgMember?.user.id, - userName: getUsername(run.environment.orgMember?.user), - }, - job: run.job, - })), - pagination: { - next, - previous, - }, - }; - } -} diff --git a/apps/webapp/app/presenters/RunPresenter.server.ts b/apps/webapp/app/presenters/RunPresenter.server.ts deleted file mode 100644 index 34a1e6f59a..0000000000 --- a/apps/webapp/app/presenters/RunPresenter.server.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { - ErrorWithStack, - ErrorWithStackSchema, - EventSpecificationSchema, - StyleSchema, -} from "@trigger.dev/core"; -import { $replica, PrismaClient, prisma } from "~/db.server"; -import { isRunCompleted, runBasicStatus } from "~/models/jobRun.server"; -import { mergeProperties } from "~/utils/mergeProperties.server"; -import { taskListToTree } from "~/utils/taskListToTree"; -import { getUsername } from "~/utils/username"; - -type RunOptions = { - id: string; - userId: string; -}; - -export type ViewRun = NonNullable>>; -export type ViewTask = NonNullable>>["tasks"][number]; -export type ViewEvent = NonNullable>>["event"]; - -type QueryEvent = NonNullable>>["event"]; -type QueryTask = NonNullable>>["tasks"][number]; - -export class RunPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ id, userId }: RunOptions) { - const run = await this.query({ id, userId }); - - if (!run) { - return undefined; - } - - const eventSpecification = EventSpecificationSchema.parse(run.version.eventSpecification); - - const runProperties = mergeProperties( - run.version.properties, - run.properties, - eventSpecification.properties - ); - - //enrich tasks then group subtasks under their parents - const enrichedTasks = this.enrichTasks(run.tasks); - const tasks = taskListToTree(enrichedTasks); - - let runError: ErrorWithStack | undefined = undefined; - let runOutput: string | null | undefined = run.output - ? JSON.stringify(run.output, null, 2) - : null; - - if (run.status === "FAILURE") { - const error = ErrorWithStackSchema.safeParse(run.output); - - if (error.success) { - runError = error.data; - runOutput = null; - } else { - runError = { message: "Unknown error" }; - runOutput = null; - } - } - - return { - id: run.id, - number: run.number, - status: run.status, - basicStatus: runBasicStatus(run.status), - isFinished: isRunCompleted(run.status), - startedAt: run.startedAt, - completedAt: run.completedAt, - isTest: run.isTest, - version: run.version.version, - output: runOutput, - properties: runProperties, - environment: { - type: run.environment.type, - slug: run.environment.slug, - userId: run.environment.orgMember?.user.id, - userName: getUsername(run.environment.orgMember?.user), - }, - event: this.#prepareEventData(run.event), - tasks, - runConnections: run.runConnections, - missingConnections: run.missingConnections, - error: runError, - executionDuration: run.executionDuration, - executionCount: run.executionCount, - }; - } - - #prepareEventData(event: QueryEvent) { - return { - id: event.eventId, - name: event.name, - payload: JSON.stringify(event.payload), - context: JSON.stringify(event.context), - timestamp: event.timestamp, - deliveredAt: event.deliveredAt, - externalAccount: event.externalAccount - ? { - identifier: event.externalAccount.identifier, - } - : undefined, - }; - } - - query({ id, userId }: RunOptions) { - return $replica.jobRun.findFirst({ - select: { - id: true, - number: true, - status: true, - startedAt: true, - completedAt: true, - isTest: true, - properties: true, - output: true, - executionCount: true, - executionDuration: true, - version: { - select: { - version: true, - properties: true, - eventSpecification: true, - }, - }, - environment: { - select: { - type: true, - slug: true, - orgMember: { - select: { - user: { - select: { - id: true, - name: true, - displayName: true, - }, - }, - }, - }, - }, - }, - event: { - select: { - eventId: true, - name: true, - payload: true, - context: true, - timestamp: true, - deliveredAt: true, - externalAccount: { - select: { - identifier: true, - }, - }, - }, - }, - tasks: { - select: { - id: true, - displayKey: true, - name: true, - icon: true, - status: true, - delayUntil: true, - description: true, - properties: true, - outputProperties: true, - error: true, - startedAt: true, - completedAt: true, - style: true, - parentId: true, - noop: true, - runConnection: { - select: { - integration: { - select: { - definitionId: true, - title: true, - slug: true, - definition: { - select: { - icon: true, - }, - }, - }, - }, - }, - }, - }, - orderBy: { - createdAt: "asc", - }, - take: 1000, - }, - runConnections: { - select: { - id: true, - key: true, - integration: { - select: { - title: true, - slug: true, - description: true, - scopes: true, - definition: true, - }, - }, - }, - }, - missingConnections: true, - }, - where: { - id, - organization: { - members: { - some: { - userId, - }, - }, - }, - }, - }); - } - - enrichTasks(tasks: QueryTask[]) { - return tasks.map((task) => ({ - ...task, - error: task.error ? ErrorWithStackSchema.parse(task.error) : undefined, - connection: task.runConnection, - properties: mergeProperties(task.properties, task.outputProperties), - style: task.style ? StyleSchema.parse(task.style) : undefined, - })); - } -} diff --git a/apps/webapp/app/presenters/RunStreamPresenter.server.ts b/apps/webapp/app/presenters/RunStreamPresenter.server.ts deleted file mode 100644 index f7c943dcca..0000000000 --- a/apps/webapp/app/presenters/RunStreamPresenter.server.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { JobRun } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { sse } from "~/utils/sse.server"; - -export class RunStreamPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ request, runId }: { request: Request; runId: JobRun["id"] }) { - const run = await this.#runForUpdates(runId); - - if (!run) { - return new Response("Not found", { status: 404 }); - } - - let lastUpdatedAt: number = run.updatedAt.getTime(); - let lastTotalTaskUpdatedTime = run.tasks.reduce( - (prev, task) => prev + task.updatedAt.getTime(), - 0 - ); - - return sse({ - request, - run: async (send, stop) => { - const result = await this.#runForUpdates(runId); - if (!result) { - return stop(); - } - - if (result.completedAt) { - send({ data: new Date().toISOString() }); - return stop(); - } - - const totalRunUpdated = result.tasks.reduce( - (prev, task) => prev + task.updatedAt.getTime(), - 0 - ); - - if (lastUpdatedAt !== result.updatedAt.getTime()) { - send({ data: result.updatedAt.toISOString() }); - } else if (lastTotalTaskUpdatedTime !== totalRunUpdated) { - send({ data: new Date().toISOString() }); - } - - lastUpdatedAt = result.updatedAt.getTime(); - lastTotalTaskUpdatedTime = totalRunUpdated; - }, - }); - } - - #runForUpdates(id: string) { - return this.#prismaClient.jobRun.findUnique({ - where: { - id, - }, - select: { - updatedAt: true, - completedAt: true, - tasks: { - select: { - updatedAt: true, - }, - }, - }, - }); - } -} diff --git a/apps/webapp/app/presenters/ScheduledTriggersPresenter.server.ts b/apps/webapp/app/presenters/ScheduledTriggersPresenter.server.ts deleted file mode 100644 index bf72bdd675..0000000000 --- a/apps/webapp/app/presenters/ScheduledTriggersPresenter.server.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ScheduleMetadataSchema } from "@trigger.dev/core"; -import { User } from "@trigger.dev/database"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { calculateNextScheduledEvent } from "~/services/schedules/nextScheduledEvent.server"; -import { BasePresenter } from "./v3/basePresenter.server"; - -const DEFAULT_PAGE_SIZE = 20; - -export class ScheduledTriggersPresenter extends BasePresenter { - public async call({ - userId, - projectSlug, - organizationSlug, - direction = "forward", - pageSize = DEFAULT_PAGE_SIZE, - cursor, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - organizationSlug: Organization["slug"]; - direction?: "forward" | "backward"; - pageSize?: number; - cursor?: string; - }) { - const organization = await this._replica.organization.findFirstOrThrow({ - select: { - id: true, - }, - where: { - slug: organizationSlug, - members: { some: { userId } }, - }, - }); - - // Find the project scoped to the organization - const project = await this._replica.project.findFirstOrThrow({ - select: { - id: true, - }, - where: { - slug: projectSlug, - organizationId: organization.id, - }, - }); - - const directionMultiplier = direction === "forward" ? 1 : -1; - - const scheduled = await this._replica.scheduleSource.findMany({ - select: { - id: true, - key: true, - active: true, - schedule: true, - lastEventTimestamp: true, - environment: { - select: { - type: true, - }, - }, - createdAt: true, - updatedAt: true, - metadata: true, - dynamicTrigger: true, - }, - where: { - environment: { - OR: [ - { - orgMember: null, - }, - { - orgMember: { - userId, - }, - }, - ], - projectId: project.id, - }, - }, - orderBy: [{ id: "desc" }], - //take an extra record to tell if there are more - take: directionMultiplier * (pageSize + 1), - //skip the cursor if there is one - skip: cursor ? 1 : 0, - cursor: cursor - ? { - id: cursor, - } - : undefined, - }); - - const hasMore = scheduled.length > pageSize; - - //get cursors for next and previous pages - let next: string | undefined; - let previous: string | undefined; - switch (direction) { - case "forward": - previous = cursor ? scheduled.at(0)?.id : undefined; - if (hasMore) { - next = scheduled[pageSize - 1]?.id; - } - break; - case "backward": - if (hasMore) { - previous = scheduled[1]?.id; - next = scheduled[pageSize]?.id; - } else { - next = scheduled[pageSize - 1]?.id; - } - break; - } - - const scheduledToReturn = - direction === "backward" && hasMore - ? scheduled.slice(1, pageSize + 1) - : scheduled.slice(0, pageSize); - - return { - scheduled: scheduledToReturn.map((s) => { - const schedule = ScheduleMetadataSchema.parse(s.schedule); - const nextEventTimestamp = s.active - ? calculateNextScheduledEvent(schedule, s.lastEventTimestamp) - : undefined; - - return { - ...s, - schedule, - nextEventTimestamp, - }; - }), - pagination: { - next, - previous, - }, - }; - } -} diff --git a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts index e87239cc53..32f666b077 100644 --- a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts +++ b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts @@ -22,7 +22,7 @@ export class SelectBestProjectPresenter { } } - //failing that, we pick the project with the most jobs + //failing that, we pick the most recently modified project const projects = await this.#prismaClient.project.findMany({ include: { organization: true, @@ -34,9 +34,7 @@ export class SelectBestProjectPresenter { }, }, orderBy: { - jobs: { - _count: "desc", - }, + updatedAt: "desc", }, take: 1, }); diff --git a/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts b/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts deleted file mode 100644 index 483f3934f3..0000000000 --- a/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { RedactSchema, StyleSchema } from "@trigger.dev/core"; -import { $replica, PrismaClient, prisma } from "~/db.server"; -import { mergeProperties } from "~/utils/mergeProperties.server"; -import { Redactor } from "~/utils/redactor"; - -type DetailsProps = { - id: string; - userId: string; -}; - -export class TaskDetailsPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ id, userId }: DetailsProps) { - const task = await $replica.task.findFirst({ - select: { - id: true, - displayKey: true, - runConnection: { - select: { - id: true, - key: true, - connection: { - select: { - metadata: true, - connectionType: true, - integration: { - select: { - title: true, - slug: true, - description: true, - scopes: true, - definition: true, - authMethod: { - select: { - type: true, - name: true, - }, - }, - }, - }, - }, - }, - }, - }, - name: true, - icon: true, - status: true, - delayUntil: true, - noop: true, - description: true, - properties: true, - outputProperties: true, - params: true, - output: true, - outputIsUndefined: true, - error: true, - startedAt: true, - completedAt: true, - style: true, - parentId: true, - redact: true, - attempts: { - select: { - number: true, - status: true, - error: true, - runAt: true, - updatedAt: true, - }, - orderBy: { - number: "asc", - }, - }, - }, - where: { - id, - }, - }); - - if (!task) { - return undefined; - } - - return { - ...task, - redact: undefined, - output: JSON.stringify( - this.#stringifyOutputWithRedactions(task.output, task.redact), - null, - 2 - ), - connection: task.runConnection, - params: task.params as Record, - properties: mergeProperties(task.properties, task.outputProperties), - style: task.style ? StyleSchema.parse(task.style) : undefined, - }; - } - - #stringifyOutputWithRedactions(output: any, redact: unknown): any { - if (!output) { - return output; - } - - const parsedRedact = RedactSchema.safeParse(redact); - - if (!parsedRedact.success) { - return output; - } - - const paths = parsedRedact.data.paths; - - const redactor = new Redactor(paths); - - return redactor.redact(output); - } -} diff --git a/apps/webapp/app/presenters/TestJobPresenter.server.ts b/apps/webapp/app/presenters/TestJobPresenter.server.ts deleted file mode 100644 index cb66659943..0000000000 --- a/apps/webapp/app/presenters/TestJobPresenter.server.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { User } from "@trigger.dev/database"; -import { replacements } from "@trigger.dev/core"; -import { PrismaClient, prisma } from "~/db.server"; -import { Job } from "~/models/job.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { EventExample } from "@trigger.dev/core"; - -export class TestJobPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - organizationSlug, - projectSlug, - jobSlug, - }: { - userId: User["id"]; - organizationSlug: Organization["slug"]; - projectSlug: Project["slug"]; - jobSlug: Job["slug"]; - }) { - const job = await this.#prismaClient.job.findFirst({ - select: { - aliases: { - select: { - version: { - select: { - id: true, - version: true, - examples: { - select: { - id: true, - name: true, - icon: true, - payload: true, - }, - }, - integrations: { - select: { - integration: { - select: { - authSource: true, - }, - }, - }, - }, - }, - }, - environment: { - select: { - id: true, - type: true, - slug: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - }, - }, - where: { - name: "latest", - environment: { - OR: [ - { - orgMember: null, - }, - { - orgMember: { - userId, - }, - }, - ], - }, - }, - }, - runs: { - select: { - id: true, - createdAt: true, - number: true, - status: true, - event: { - select: { - payload: true, - }, - }, - }, - orderBy: { - createdAt: "desc", - }, - take: 5, - }, - }, - where: { - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, - project: { - slug: projectSlug, - }, - slug: jobSlug, - }, - }); - - if (!job) { - throw new Error("Job not found"); - } - - //collect together the examples, we don't care about the environments - const examples = job.aliases.flatMap((alias) => - alias.version.examples.map((example) => ({ - ...example, - icon: example.icon ?? undefined, - payload: example.payload ? JSON.stringify(example.payload, exampleReplacer, 2) : undefined, - })) - ); - - return { - environments: job.aliases.map((alias) => ({ - id: alias.environment.id, - type: alias.environment.type, - slug: alias.environment.slug, - userId: alias.environment.orgMember?.userId, - versionId: alias.version.id, - hasAuthResolver: alias.version.integrations.some( - (i) => i.integration.authSource === "RESOLVER" - ), - })), - examples, - runs: job.runs.map((r) => ({ - id: r.id, - number: r.number, - status: r.status, - created: r.createdAt, - payload: r.event.payload ? JSON.stringify(r.event.payload, null, 2) : undefined, - })), - }; - } -} - -function exampleReplacer(key: string, value: any) { - replacements.forEach((replacement) => { - if (value === replacement.marker) { - value = replacement.replace({ - match: { - key, - value, - }, - data: { now: new Date() }, - }); - } - }); - - return value; -} diff --git a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts b/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts deleted file mode 100644 index 7a93e77ac9..0000000000 --- a/apps/webapp/app/presenters/TriggerDetailsPresenter.server.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PrismaClient, prisma } from "~/db.server"; - -export type DetailedEvent = NonNullable>>; - -export class TriggerDetailsPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call(runId: string) { - const { event } = await this.#prismaClient.jobRun.findUniqueOrThrow({ - where: { - id: runId, - }, - select: { - event: { - select: { - eventId: true, - name: true, - payload: true, - context: true, - timestamp: true, - deliveredAt: true, - externalAccount: { - select: { - identifier: true, - }, - }, - }, - }, - }, - }); - - return { - id: event.eventId, - name: event.name, - payload: JSON.stringify(event.payload, null, 2), - context: JSON.stringify(event.context, null, 2), - timestamp: event.timestamp, - deliveredAt: event.deliveredAt, - externalAccount: event.externalAccount - ? { - identifier: event.externalAccount.identifier, - } - : undefined, - }; - } -} diff --git a/apps/webapp/app/presenters/TriggerSourcePresenter.server.ts b/apps/webapp/app/presenters/TriggerSourcePresenter.server.ts deleted file mode 100644 index 12552d6848..0000000000 --- a/apps/webapp/app/presenters/TriggerSourcePresenter.server.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { TriggerSource, User } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { RunList, RunListPresenter } from "./RunListPresenter.server"; -import { Direction } from "~/components/runs/RunStatuses"; - -export class TriggerSourcePresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - triggerSourceId, - direction = "forward", - cursor, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - organizationSlug: Organization["slug"]; - triggerSourceId: TriggerSource["id"]; - direction?: Direction; - cursor?: string; - }) { - const trigger = await this.#prismaClient.triggerSource.findUnique({ - select: { - id: true, - active: true, - integration: { - select: { - id: true, - title: true, - slug: true, - definitionId: true, - setupStatus: true, - definition: { - select: { - icon: true, - }, - }, - }, - }, - environment: { - select: { - type: true, - }, - }, - createdAt: true, - updatedAt: true, - params: true, - sourceRegistrationJob: { - select: { - job: { - select: { - id: true, - slug: true, - }, - }, - }, - }, - dynamicTrigger: { - select: { - id: true, - slug: true, - sourceRegistrationJob: { - select: { - job: { - select: { - id: true, - slug: true, - }, - }, - }, - }, - }, - }, - }, - where: { - id: triggerSourceId, - }, - }); - - if (!trigger) { - throw new Error("Trigger source not found"); - } - - const runListPresenter = new RunListPresenter(this.#prismaClient); - const jobSlug = getJobSlug( - trigger.sourceRegistrationJob?.job.slug, - trigger.dynamicTrigger?.sourceRegistrationJob?.job.slug - ); - - const runList = jobSlug - ? await runListPresenter.call({ - userId, - jobSlug, - organizationSlug, - projectSlug, - direction, - cursor, - }) - : undefined; - - return { - trigger: { - id: trigger.id, - active: trigger.active, - integration: trigger.integration, - environment: trigger.environment, - createdAt: trigger.createdAt, - updatedAt: trigger.updatedAt, - params: trigger.params, - registrationJob: trigger.sourceRegistrationJob?.job, - runList, - dynamic: trigger.dynamicTrigger - ? { id: trigger.dynamicTrigger.id, slug: trigger.dynamicTrigger.slug } - : undefined, - }, - }; - } -} - -function getJobSlug( - sourceRegistrationJobSlug: string | undefined, - dynamicSourceRegistrationJobSlug: string | undefined -) { - if (sourceRegistrationJobSlug) { - return sourceRegistrationJobSlug; - } - - return dynamicSourceRegistrationJobSlug; -} diff --git a/apps/webapp/app/presenters/TriggersPresenter.server.ts b/apps/webapp/app/presenters/TriggersPresenter.server.ts deleted file mode 100644 index 8c13e92e30..0000000000 --- a/apps/webapp/app/presenters/TriggersPresenter.server.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { User } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; - -export class TriggersPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - organizationSlug: Organization["slug"]; - }) { - const triggers = await this.#prismaClient.triggerSource.findMany({ - select: { - id: true, - active: true, - dynamicTrigger: { - select: { - id: true, - slug: true, - }, - }, - integration: { - select: { - id: true, - title: true, - slug: true, - definitionId: true, - setupStatus: true, - definition: { - select: { - icon: true, - }, - }, - }, - }, - environment: { - select: { - type: true, - }, - }, - createdAt: true, - updatedAt: true, - params: true, - registrations: true, - sourceRegistrationJob: true, - }, - where: { - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, - environment: { - OR: [ - { - orgMember: null, - }, - { - orgMember: { - userId, - }, - }, - ], - }, - project: { - slug: projectSlug, - }, - }, - }); - - return { - triggers, - }; - } -} diff --git a/apps/webapp/app/presenters/WebhookDeliveryListPresenter.server.ts b/apps/webapp/app/presenters/WebhookDeliveryListPresenter.server.ts deleted file mode 100644 index c83a7aeb42..0000000000 --- a/apps/webapp/app/presenters/WebhookDeliveryListPresenter.server.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Direction } from "~/components/runs/RunStatuses"; -import { PrismaClient, prisma } from "~/db.server"; - -type RunListOptions = { - userId: string; - webhookId: string; - direction?: Direction; - cursor?: string; -}; - -const PAGE_SIZE = 20; - -export type WebhookDeliveryList = Awaited>; - -export class WebhookDeliveryListPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ userId, webhookId, direction = "forward", cursor }: RunListOptions) { - const directionMultiplier = direction === "forward" ? 1 : -1; - - const runs = await this.#prismaClient.webhookRequestDelivery.findMany({ - select: { - id: true, - number: true, - createdAt: true, - deliveredAt: true, - verified: true, - error: true, - environment: { - select: { - type: true, - slug: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - }, - }, - where: { - webhookId, - environment: { - OR: [ - { - orgMember: null, - }, - { - orgMember: { - userId, - }, - }, - ], - }, - }, - orderBy: [{ id: "desc" }], - //take an extra page to tell if there are more - take: directionMultiplier * (PAGE_SIZE + 1), - //skip the cursor if there is one - skip: cursor ? 1 : 0, - cursor: cursor - ? { - id: cursor, - } - : undefined, - }); - - const hasMore = runs.length > PAGE_SIZE; - - //get cursors for next and previous pages - let next: string | undefined; - let previous: string | undefined; - switch (direction) { - case "forward": - previous = cursor ? runs.at(0)?.id : undefined; - if (hasMore) { - next = runs[PAGE_SIZE - 1]?.id; - } - break; - case "backward": - if (hasMore) { - previous = runs[1]?.id; - next = runs[PAGE_SIZE]?.id; - } else { - next = runs[PAGE_SIZE - 1]?.id; - } - break; - } - - const runsToReturn = - direction === "backward" && hasMore ? runs.slice(1, PAGE_SIZE + 1) : runs.slice(0, PAGE_SIZE); - - return { - runs: runsToReturn.map((run) => ({ - id: run.id, - number: run.number, - createdAt: run.createdAt, - deliveredAt: run.deliveredAt, - verified: run.verified, - error: run.error, - environment: { - type: run.environment.type, - slug: run.environment.slug, - userId: run.environment.orgMember?.userId, - }, - })), - pagination: { - next, - previous, - }, - }; - } -} diff --git a/apps/webapp/app/presenters/WebhookDeliveryPresenter.server.ts b/apps/webapp/app/presenters/WebhookDeliveryPresenter.server.ts deleted file mode 100644 index cb831834b1..0000000000 --- a/apps/webapp/app/presenters/WebhookDeliveryPresenter.server.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { User, Webhook } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { organizationPath, projectPath } from "~/utils/pathBuilder"; -import { WebhookDeliveryListPresenter } from "./WebhookDeliveryListPresenter.server"; -import { Direction } from "~/components/runs/RunStatuses"; - -export class WebhookDeliveryPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - webhookId, - direction = "forward", - cursor, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - organizationSlug: Organization["slug"]; - webhookId: Webhook["id"]; - direction?: Direction; - cursor?: string; - }) { - const webhook = await this.#prismaClient.webhook.findUnique({ - select: { - id: true, - key: true, - active: true, - integration: { - select: { - id: true, - title: true, - slug: true, - definitionId: true, - setupStatus: true, - definition: { - select: { - icon: true, - }, - }, - }, - }, - httpEndpoint: { - select: { - key: true, - }, - }, - createdAt: true, - updatedAt: true, - params: true, - }, - where: { - id: webhookId, - }, - }); - - if (!webhook) { - throw new Error("Webhook source not found"); - } - - const deliveryListPresenter = new WebhookDeliveryListPresenter(this.#prismaClient); - - const orgRootPath = organizationPath({ slug: organizationSlug }); - const projectRootPath = projectPath({ slug: organizationSlug }, { slug: projectSlug }); - - const requestDeliveries = await deliveryListPresenter.call({ - userId, - webhookId: webhook.id, - direction, - cursor, - }); - - return { - webhook: { - id: webhook.id, - key: webhook.key, - active: webhook.active, - integration: webhook.integration, - integrationLink: `${orgRootPath}/integrations/${webhook.integration.slug}`, - httpEndpoint: webhook.httpEndpoint, - httpEndpointLink: `${projectRootPath}/http-endpoints/${webhook.httpEndpoint.key}`, - createdAt: webhook.createdAt, - updatedAt: webhook.updatedAt, - params: webhook.params, - requestDeliveries, - }, - }; - } -} diff --git a/apps/webapp/app/presenters/WebhookSourcePresenter.server.ts b/apps/webapp/app/presenters/WebhookSourcePresenter.server.ts deleted file mode 100644 index 536ad4eb59..0000000000 --- a/apps/webapp/app/presenters/WebhookSourcePresenter.server.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { User, Webhook } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { Organization } from "~/models/organization.server"; -import { Project } from "~/models/project.server"; -import { RunListPresenter } from "./RunListPresenter.server"; -import { organizationPath, projectPath } from "~/utils/pathBuilder"; -import { Direction } from "~/components/runs/RunStatuses"; - -export class WebhookSourcePresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - webhookId, - direction = "forward", - cursor, - getDeliveryRuns = false, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - organizationSlug: Organization["slug"]; - webhookId: Webhook["id"]; - direction?: Direction; - cursor?: string; - getDeliveryRuns?: boolean; - }) { - const webhook = await this.#prismaClient.webhook.findUnique({ - select: { - id: true, - key: true, - active: true, - integration: { - select: { - id: true, - title: true, - slug: true, - definitionId: true, - setupStatus: true, - definition: { - select: { - icon: true, - }, - }, - }, - }, - httpEndpoint: { - select: { - key: true, - }, - }, - createdAt: true, - updatedAt: true, - params: true, - }, - where: { - id: webhookId, - }, - }); - - if (!webhook) { - throw new Error("Webhook source not found"); - } - - const runListPresenter = new RunListPresenter(this.#prismaClient); - const jobSlug = getDeliveryRuns - ? getDeliveryJobSlug(webhook.key) - : getRegistrationJobSlug(webhook.key); - - const runList = await runListPresenter.call({ - userId, - jobSlug, - organizationSlug, - projectSlug, - direction, - cursor, - }); - - const orgRootPath = organizationPath({ slug: organizationSlug }); - const projectRootPath = projectPath({ slug: organizationSlug }, { slug: projectSlug }); - - return { - trigger: { - id: webhook.id, - key: webhook.key, - active: webhook.active, - integration: webhook.integration, - integrationLink: `${orgRootPath}/integrations/${webhook.integration.slug}`, - httpEndpoint: webhook.httpEndpoint, - httpEndpointLink: `${projectRootPath}/http-endpoints/${webhook.httpEndpoint.key}`, - createdAt: webhook.createdAt, - updatedAt: webhook.updatedAt, - params: webhook.params, - runList, - }, - }; - } -} - -const getRegistrationJobSlug = (key: string) => `webhook.register.${key}`; - -const getDeliveryJobSlug = (key: string) => `webhook.deliver.${key}`; diff --git a/apps/webapp/app/presenters/WebhookTriggersPresenter.server.ts b/apps/webapp/app/presenters/WebhookTriggersPresenter.server.ts deleted file mode 100644 index a9e76cae54..0000000000 --- a/apps/webapp/app/presenters/WebhookTriggersPresenter.server.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Organization, User } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { Project } from "~/models/project.server"; - -export class WebhookTriggersPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - }: { - userId: User["id"]; - projectSlug: Project["slug"]; - organizationSlug: Organization["slug"]; - }) { - const webhooks = await this.#prismaClient.webhook.findMany({ - select: { - id: true, - key: true, - active: true, - params: true, - integration: { - select: { - id: true, - title: true, - slug: true, - definitionId: true, - setupStatus: true, - definition: { - select: { - icon: true, - }, - }, - }, - }, - webhookEnvironments: { - select: { - id: true, - environment: { - select: { - type: true - } - } - } - }, - createdAt: true, - updatedAt: true, - }, - where: { - project: { - slug: projectSlug, - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, - }, - }, - }); - - return { webhooks }; - } -} diff --git a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts index ef7d3f2dd4..855c75a8ba 100644 --- a/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BatchListPresenter.server.ts @@ -1,9 +1,9 @@ import { BatchTaskRunStatus, Prisma } from "@trigger.dev/database"; import parse from "parse-duration"; -import { type Direction } from "~/components/runs/RunStatuses"; import { sqlDatabaseSchema } from "~/db.server"; import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; import { BasePresenter } from "./basePresenter.server"; +import { Direction } from "~/components/ListPagination"; export type BatchListOptions = { userId?: string; diff --git a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts index 208466544c..590e499a77 100644 --- a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts @@ -1,11 +1,11 @@ import { Prisma, type TaskRunStatus } from "@trigger.dev/database"; import parse from "parse-duration"; -import { type Direction } from "~/components/runs/RunStatuses"; import { sqlDatabaseSchema } from "~/db.server"; import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; import { isCancellableRunStatus, isFinalRunStatus } from "~/v3/taskStatus"; import { BasePresenter } from "./basePresenter.server"; import { getAllTaskIdentifiers } from "~/models/task.server"; +import { Direction } from "~/components/ListPagination"; export type RunListOptions = { userId?: string; diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index 409fd120f0..e1418968de 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -7,12 +7,10 @@ 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 { Toast } from "./components/primitives/Toast"; import { env } from "./env.server"; import { featuresForRequest } from "./features.server"; -import { useHighlight } from "./hooks/useHighlight"; import { usePostHog } from "./hooks/usePostHog"; import { getUser } from "./services/session.server"; import { appEnvTitleTag } from "./utils"; @@ -40,7 +38,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const session = await getSession(request.headers.get("cookie")); const toastMessage = session.get("toastMessage") as ToastMessage; const posthogProjectKey = env.POSTHOG_PROJECT_KEY; - const highlightProjectId = env.HIGHLIGHT_PROJECT_ID; const features = featuresForRequest(request); return typedjson( @@ -48,7 +45,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { user: await getUser(request), toastMessage, posthogProjectKey, - highlightProjectId, features, appEnv: env.APP_ENV, appOrigin: env.APP_ORIGIN, @@ -91,19 +87,11 @@ export function ErrorBoundary() { } function App() { - const { posthogProjectKey, highlightProjectId } = useTypedLoaderData(); + const { posthogProjectKey } = useTypedLoaderData(); usePostHog(posthogProjectKey); - useHighlight(); return ( <> - {highlightProjectId && ( - - )} diff --git a/apps/webapp/app/routes/_app._index/route.tsx b/apps/webapp/app/routes/_app._index/route.tsx index dc9a9a9dc1..041f8aee11 100644 --- a/apps/webapp/app/routes/_app._index/route.tsx +++ b/apps/webapp/app/routes/_app._index/route.tsx @@ -3,7 +3,12 @@ import { prisma } from "~/db.server"; import { getUsersInvites } from "~/models/member.server"; import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server"; import { requireUser } from "~/services/session.server"; -import { invitesPath, newOrganizationPath, newProjectPath, projectPath } from "~/utils/pathBuilder"; +import { + invitesPath, + newOrganizationPath, + newProjectPath, + v3ProjectPath, +} from "~/utils/pathBuilder"; //this loader chooses the best project to redirect you to, ideally based on the cookie export const loader = async ({ request }: LoaderFunctionArgs) => { @@ -19,7 +24,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { try { const { project, organization } = await presenter.call({ userId: user.id, request }); //redirect them to the most appropriate project - return redirect(projectPath(organization, project)); + return redirect(v3ProjectPath(organization, project)); } catch (e) { const organization = await prisma.organization.findFirst({ where: { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.canceled/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.canceled/route.tsx deleted file mode 100644 index a2f85995aa..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.canceled/route.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; -import { z } from "zod"; -import { prisma } from "~/db.server"; -import { redirectWithErrorMessage } from "~/models/message.server"; -import { plansPath } from "~/utils/pathBuilder"; - -const ParamsSchema = z.object({ - organizationId: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { organizationId } = ParamsSchema.parse(params); - - const org = await prisma.organization.findUnique({ - select: { - slug: true, - }, - where: { - id: organizationId, - }, - }); - - if (!org) { - throw new Response(null, { status: 404 }); - } - - return redirectWithErrorMessage( - `${plansPath({ slug: org.slug })}`, - request, - "You didn't complete your details on Stripe. Please try again." - ); -}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.complete/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.complete/route.tsx deleted file mode 100644 index 0282294290..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.complete/route.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { z } from "zod"; -import { prisma } from "~/db.server"; -import { redirectWithSuccessMessage } from "~/models/message.server"; -import { subscribedPath } from "~/utils/pathBuilder"; - -const ParamsSchema = z.object({ - organizationId: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { organizationId } = ParamsSchema.parse(params); - - const org = await prisma.organization.findUnique({ - select: { - slug: true, - }, - where: { - id: organizationId, - }, - }); - - if (!org) { - throw new Response(null, { status: 404 }); - } - - return redirectWithSuccessMessage( - `${subscribedPath({ slug: org.slug })}`, - request, - "You are now subscribed to Trigger.dev" - ); -}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.failed/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.failed/route.tsx deleted file mode 100644 index efe9ddb578..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.failed/route.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { z } from "zod"; -import { prisma } from "~/db.server"; -import { redirectWithErrorMessage } from "~/models/message.server"; -import { plansPath } from "~/utils/pathBuilder"; - -const ParamsSchema = z.object({ - organizationId: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { organizationId } = ParamsSchema.parse(params); - - const org = await prisma.organization.findUnique({ - select: { - slug: true, - }, - where: { - id: organizationId, - }, - }); - - if (!org) { - throw new Response(null, { status: 404 }); - } - - const url = new URL(request.url); - const searchParams = new URLSearchParams(url.search); - const reason = searchParams.get("reason"); - - let errorMessage = reason ? decodeURIComponent(reason) : "Subscribing failed to complete"; - - return redirectWithErrorMessage(`${plansPath({ slug: org.slug })}`, request, errorMessage); -}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx index 85c1780ffa..34f098a241 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx @@ -1,13 +1,13 @@ +import { FolderIcon } from "@heroicons/react/20/solid"; import { Link, MetaFunction } from "@remix-run/react"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { LinkButton } from "~/components/primitives/Buttons"; import { Header3 } from "~/components/primitives/Headers"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { useOrganization } from "~/hooks/useOrganizations"; -import { newProjectPath, projectPath } from "~/utils/pathBuilder"; +import { newProjectPath, v3ProjectPath } from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { return [ @@ -45,9 +45,9 @@ export default function Page() {
  • - +
    {project.name} {project.version} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing._index/route.tsx deleted file mode 100644 index 2bcc152517..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing._index/route.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { ArrowRightIcon } from "@heroicons/react/20/solid"; -import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; -import { Await, useLoaderData } from "@remix-run/react"; -import { DataFunctionArgs, defer } from "@remix-run/server-runtime"; -import { Suspense } from "react"; -import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts"; -import { ConcurrentRunsChart } from "~/components/billing/v2/ConcurrentRunsChart"; -import { UsageBar } from "~/components/billing/v2/UsageBar"; -import { LinkButton } from "~/components/primitives/Buttons"; -import { Callout } from "~/components/primitives/Callout"; -import { DailyRunsChart } from "~/components/billing/v2/DailyRunsChat"; -import { DateTime } from "~/components/primitives/DateTime"; -import { Header2, Header3 } from "~/components/primitives/Headers"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { Spinner } from "~/components/primitives/Spinner"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server"; -import { requireUserId } from "~/services/session.server"; -import { formatCurrency, formatNumberCompact } from "~/utils/numberFormatter"; -import { OrganizationParamsSchema, plansPath } from "~/utils/pathBuilder"; -import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; - -export async function loader({ request, params }: DataFunctionArgs) { - const userId = await requireUserId(request); - const { organizationSlug } = OrganizationParamsSchema.parse(params); - - const presenter = new OrgUsagePresenter(); - const usageData = presenter.call({ userId, slug: organizationSlug, request }); - return defer({ usageData }); -} - -const CustomTooltip = ({ active, payload, label }: TooltipProps) => { - if (active && payload) { - return ( -
    -

    {label}:

    -

    {payload[0].value}

    -
    - ); - } - - return null; -}; - -export default function Page() { - const organization = useOrganization(); - const { usageData } = useLoaderData(); - const currentPlan = useCurrentPlan(); - - const hitsRunLimit = currentPlan?.usage?.runCountCap - ? currentPlan.usage.currentRunCount > currentPlan.usage.runCountCap - : false; - - return ( -
    - - - - - } - > - There was a problem loading your usage data.} - > - {(data) => { - const hitConcurrencyLimit = currentPlan?.subscription?.limits.concurrentRuns - ? data.concurrencyData.some( - (c) => - c.maxConcurrentRuns >= - (currentPlan.subscription?.limits.concurrentRuns ?? Infinity) - ) - : false; - - return ( - <> -
    - Concurrent runs -
    - {hitConcurrencyLimit && ( - - Increase concurrent runs - - } - > - {`Some of your runs are being queued because the number of concurrent runs is limited to - ${currentPlan?.subscription?.limits.concurrentRuns}.`} - - )} - -
    -
    - -
    - Runs -
    - {hitsRunLimit && ( - - Upgrade - - } - > - - You have exceeded the monthly{" "} - {formatNumberCompact(currentPlan?.subscription?.limits.runs ?? 0)} runs - limit. Upgrade to a paid plan before{" "} - - . - - - )} -
    -
    - {data.runCostEstimation !== undefined && - data.projectedRunCostEstimation !== undefined && ( -
    -
    - Month-to-date -

    - {formatCurrency(data.runCostEstimation, false)} -

    -
    - -
    - Projected -

    - {formatCurrency(data.projectedRunCostEstimation, false)} -

    -
    -
    - )} - -
    -
    - Monthly runs - {!data.hasMonthlyRunData && ( - - No runs to show - - )} - - - - `${value}`} - /> - } - /> - - - -
    -
    -
    - Daily runs - -
    -
    -
    - - ); - }} -
    -
    -
    - ); -} - -function LoadingElement({ title }: { title: string }) { - return ( -
    - {title} -
    - -
    -
    - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing.plans/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing.plans/route.tsx deleted file mode 100644 index 5406cb0c85..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing.plans/route.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; -import { PricingCalculator } from "~/components/billing/v2/PricingCalculator"; -import { PricingTiers } from "~/components/billing/v2/PricingTiers"; -import { RunsVolumeDiscountTable } from "~/components/billing/v2/RunsVolumeDiscountTable"; -import { Callout } from "~/components/primitives/Callout"; -import { Header2 } from "~/components/primitives/Headers"; -import { featuresForRequest } from "~/features.server"; -import { OrgBillingPlanPresenter } from "~/presenters/OrgBillingPlanPresenter"; -import { formatNumberCompact } from "~/utils/numberFormatter"; -import { OrganizationParamsSchema, organizationBillingPath } from "~/utils/pathBuilder"; -import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; - -export async function loader({ params, request }: LoaderFunctionArgs) { - const { organizationSlug } = OrganizationParamsSchema.parse(params); - - const { isManagedCloud } = featuresForRequest(request); - if (!isManagedCloud) { - return redirect(organizationBillingPath({ slug: organizationSlug })); - } - - const presenter = new OrgBillingPlanPresenter(); - const result = await presenter.call({ slug: organizationSlug, isManagedCloud }); - if (!result) { - throw new Response(null, { status: 404 }); - } - - return typedjson({ - plans: result.plans, - maxConcurrency: result.maxConcurrency, - organizationSlug, - }); -} - -export default function Page() { - const { plans, maxConcurrency, organizationSlug } = useTypedLoaderData(); - const currentPlan = useCurrentPlan(); - - const hitConcurrencyLimit = - currentPlan?.subscription?.limits.concurrentRuns && maxConcurrency - ? maxConcurrency >= currentPlan.subscription!.limits.concurrentRuns! - : false; - - const hitRunLimit = currentPlan?.usage?.runCountCap - ? currentPlan.usage.currentRunCount > currentPlan.usage.runCountCap - : false; - - return ( -
    - {hitConcurrencyLimit && ( - - Some of your runs are being queued because your run concurrency is limited to{" "} - {currentPlan?.subscription?.limits.concurrentRuns}. - - )} - {hitRunLimit && ( - - {`You have exceeded the monthly - ${formatNumberCompact(currentPlan!.subscription!.limits.runs!)} runs limit. Upgrade so you - can continue to perform runs.`} - - )} - -
    - Estimate your usage -
    - -
    - -
    -
    -
    - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx deleted file mode 100644 index 0c91c510e2..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { CalendarDaysIcon, ReceiptRefundIcon } from "@heroicons/react/20/solid"; -import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; -import { Outlet } from "@remix-run/react"; -import { ActiveSubscription } from "@trigger.dev/platform/v2"; -import { formatDurationInDays } from "@trigger.dev/core/v3"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { LinkButton } from "~/components/primitives/Buttons"; -import { DateTime } from "~/components/primitives/DateTime"; -import { - PageAccessories, - NavBar, - PageInfoGroup, - PageInfoProperty, - PageInfoRow, - PageTabs, - PageTitle, -} from "~/components/primitives/PageHeader"; -import { useFeatures } from "~/hooks/useFeatures"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { plansPath, stripePortalPath, usagePath } from "~/utils/pathBuilder"; -import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; -import { Callout } from "~/components/primitives/Callout"; - -function planLabel(subscription: ActiveSubscription | undefined, periodEnd: Date) { - if (!subscription) { - return "You're currently on the Free plan"; - } - if (!subscription.isPaying) { - return `You're currently on the ${subscription.plan.title} plan`; - } - const costDescription = subscription.plan.concurrentRuns.pricing - ? `\$${subscription.plan.concurrentRuns.pricing.tierCost}/mo` - : ""; - if (subscription.canceledAt) { - return ( - <> - You're on the {costDescription} {subscription.plan.title} plan until{" "} - when you'll be on the Free plan - - ); - } - - return `You're currently on the ${costDescription} ${subscription.plan.title} plan`; -} - -export default function Page() { - const organization = useOrganization(); - const { isManagedCloud } = useFeatures(); - const currentPlan = useCurrentPlan(); - - const hasV3Project = organization.projects.some((p) => p.version === "V3"); - const hasV2Project = organization.projects.some((p) => p.version === "V2"); - const allV3Projects = organization.projects.every((p) => p.version === "V3"); - - return ( - - - - - {isManagedCloud && ( - <> - {currentPlan?.subscription?.isPaying && ( - <> - - Invoices - - - Manage card details - - - )} - {hasV2Project && ( - - Upgrade - - )} - - )} - - - -
    -
    - {hasV3Project ? ( - - This organization has a mix of v2 and v3 projects. They have separate subscriptions, - this is the usage and billing for v2. - - ) : null} - {hasV2Project && ( - - - {currentPlan?.subscription && currentPlan.usage && ( - } - value={planLabel(currentPlan.subscription, currentPlan.usage.periodEnd)} - /> - )} - {currentPlan?.subscription?.isPaying && currentPlan.usage && ( - } - label={"Billing period"} - value={ - <> - to{" "} - ( - {formatDurationInDays(currentPlan.usage.periodRemainingDuration)}{" "} - remaining) - - } - /> - )} - - - )} - {hasV2Project && isManagedCloud && ( - - )} -
    - {hasV2Project && ( -
    - -
    - )} -
    -
    -
    - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx deleted file mode 100644 index 6a790b7171..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations/route.tsx +++ /dev/null @@ -1,470 +0,0 @@ -import { ChevronRightIcon } from "@heroicons/react/24/solid"; -import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { useState } from "react"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { IntegrationIcon } from "~/assets/icons/IntegrationIcon"; -import { Feedback } from "~/components/Feedback"; -import { HowToConnectAnIntegration } from "~/components/helpContent/HelpContentText"; -import { ConnectToIntegrationSheet } from "~/components/integrations/ConnectToIntegrationSheet"; -import { IntegrationWithMissingFieldSheet } from "~/components/integrations/IntegrationWithMissingFieldSheet"; -import { NoIntegrationSheet } from "~/components/integrations/NoIntegrationSheet"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { LinkButton } from "~/components/primitives/Buttons"; -import { Callout } from "~/components/primitives/Callout"; -import { DateTime } from "~/components/primitives/DateTime"; -import { DetailCell } from "~/components/primitives/DetailCell"; -import { Header2 } from "~/components/primitives/Headers"; -import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; -import { Input } from "~/components/primitives/Input"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; -import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; -import { Switch } from "~/components/primitives/Switch"; -import { - Table, - TableBlankRow, - TableBody, - TableCell, - TableCellChevron, - TableHeader, - TableHeaderCell, - TableRow, -} from "~/components/primitives/Table"; -import { SimpleTooltip } from "~/components/primitives/Tooltip"; -import { MatchedOrganization, useOrganization } from "~/hooks/useOrganizations"; -import { useTextFilter } from "~/hooks/useTextFilter"; -import { - Client, - IntegrationOrApi, - IntegrationsPresenter, -} from "~/presenters/IntegrationsPresenter.server"; -import { requireUserId } from "~/services/session.server"; -import { OrganizationParamsSchema, docsPath, integrationClientPath } from "~/utils/pathBuilder"; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); - const { organizationSlug } = OrganizationParamsSchema.parse(params); - - const presenter = new IntegrationsPresenter(); - const data = await presenter.call({ - userId, - organizationSlug, - }); - - return typedjson(data); -}; - -export default function Integrations() { - const { clients, clientMissingFields, options, callbackUrl } = - useTypedLoaderData(); - const organization = useOrganization(); - - return ( - - - - - - Integrations documentation - - - - - -
    - -
    - {clientMissingFields.length > 0 && ( - - )} - -
    -
    -
    -
    - ); -} - -function PossibleIntegrationsList({ - options, - organizationId, - callbackUrl, -}: { - options: IntegrationOrApi[]; - organizationId: string; - callbackUrl: string; -}) { - const [onlyShowIntegrations, setOnlyShowIntegrations] = useState(false); - const optionsToShow = onlyShowIntegrations - ? options.filter((o) => o.type === "integration") - : options; - const { filterText, setFilterText, filteredItems } = useTextFilter({ - items: optionsToShow, - filter: (integration, text) => integration.name.toLowerCase().includes(text.toLowerCase()), - }); - - return ( -
    -
    -
    - Connect an API - - Trigger.dev Integrations - - } - /> -
    - setFilterText(e.target.value)} - /> -
    - {filteredItems.map((option) => { - switch (option.type) { - case "integration": - return ( - - } - /> - ); - case "api": - return ( - - } - /> - ); - } - })} -
    - Missing an API? - - - - } - defaultValue="feature" - /> - - Create an Integration - - - -
    -
    - ); -} - -function ConnectedIntegrationsList({ - clients, - organization, -}: { - clients: Client[]; - organization: MatchedOrganization; -}) { - const { filterText, setFilterText, filteredItems } = useTextFilter({ - items: clients, - filter: (client, text) => { - if (client.title.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if ( - client.customClientId && - client.customClientId.toLowerCase().includes(text.toLowerCase()) - ) { - return true; - } - - if (client.integration.name.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (client.authMethod.name.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - return false; - }, - }); - - return ( - - - - -
    - Your connected Integrations -
    - {clients.length > 0 && ( -
    -
    - setFilterText(e.target.value)} - /> - -
    - - - - Name - API - ID - Type - Jobs - Scopes - Client id - Connections - Added - Go to page - - - - {filteredItems.length === 0 ? ( - -
    - - No connected Integrations match your filters. - -
    -
    - ) : ( - <> - {filteredItems.map((client) => { - const path = integrationClientPath(organization, client); - return ( - - {client.title} - - - - {client.integration.name} - - - {client.slug} - {client.authMethod.name} - {client.jobCount} - - {client.authSource === "LOCAL" ? "–" : client.scopesCount} - - - {client.authSource === "LOCAL" ? ( - "–" - ) : ( - Auto - ) - } - content={ - client.customClientId - ? client.customClientId - : "This uses the Trigger.dev OAuth client" - } - /> - )} - - - {client.authSource === "LOCAL" ? "–" : client.connectionsCount} - - - - - - - ); - })} - - )} -
    -
    -
    - )} -
    - ); -} - -function IntegrationsWithMissingFields({ - clients, - organizationId, - callbackUrl, - options, -}: { - clients: Client[]; - organizationId: string; - callbackUrl: string; - options: IntegrationOrApi[]; -}) { - const integrationsList = options.flatMap((o) => (o.type === "integration" ? [o] : [])); - - return ( -
    - - - Integrations requiring configuration - - - - - - Name - API - Added - Go to page - - - - {clients.map((client) => { - const integration = integrationsList.find( - (i) => i.identifier === client.integrationIdentifier - ); - - if (!integration) { - return
    Can't find matching integration
    ; - } - - return ( - - - - {client.title} - - } - callbackUrl={callbackUrl} - existingIntegration={client} - className="flex w-full cursor-pointer justify-start" - /> - - - - - {client.integration.name} - - } - callbackUrl={callbackUrl} - existingIntegration={client} - className="flex w-full cursor-pointer justify-start" - /> - - - } - callbackUrl={callbackUrl} - existingIntegration={client} - className="flex w-full cursor-pointer justify-start" - /> - - - - } - callbackUrl={callbackUrl} - existingIntegration={client} - className="flex w-full cursor-pointer justify-end" - /> - - - ); - })} -
    -
    -
    - ); -} - -function AddIntegrationConnection({ - identifier, - name, - isIntegration, - icon, -}: { - identifier: string; - name: string; - isIntegration: boolean; - icon?: string; -}) { - return ( - - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam._index/route.tsx deleted file mode 100644 index ddd9b1dd46..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam._index/route.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { HowToUseThisIntegration } from "~/components/helpContent/HelpContentText"; -import { JobsTable } from "~/components/jobs/JobsTable"; -import { Callout } from "~/components/primitives/Callout"; -import { Help, HelpContent, HelpTrigger } from "~/components/primitives/Help"; -import { Input } from "~/components/primitives/Input"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { useFilterJobs } from "~/hooks/useFilterJobs"; -import { useIntegrationClient } from "~/hooks/useIntegrationClient"; -import { JobListPresenter } from "~/presenters/JobListPresenter.server"; -import { requireUserId } from "~/services/session.server"; -import { cn } from "~/utils/cn"; -import { IntegrationClientParamSchema, docsIntegrationPath } from "~/utils/pathBuilder"; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); - const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); - - const jobsPresenter = new JobListPresenter(); - - const jobs = await jobsPresenter.call({ - userId, - organizationSlug, - integrationSlug: clientParam, - }); - - return typedjson({ jobs }); -}; - -export default function Page() { - const { jobs } = useTypedLoaderData(); - const client = useIntegrationClient(); - - const { filterText, setFilterText, filteredItems } = useFilterJobs(jobs); - - return ( - - {(open) => ( -
    -
    -
    - {jobs.length !== 0 && ( - setFilterText(e.target.value)} - /> - )} - -
    - {jobs.length === 0 ? ( -
    - Jobs using this Integration will appear here. -
    - ) : ( - - )} -
    - - - - View the docs to learn more about using the {client.integration.name} Integration. - - -
    - )} -
    - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.connections/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.connections/route.tsx deleted file mode 100644 index 30a12e0223..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.connections/route.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { LoaderFunctionArgs } 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, - TableBlankRow, - TableBody, - TableCell, - TableHeader, - TableHeaderCell, - TableRow, -} from "~/components/primitives/Table"; -import { IntegrationClientConnectionsPresenter } from "~/presenters/IntegrationClientConnectionsPresenter.server"; -import { requireUserId } from "~/services/session.server"; -import { IntegrationClientParamSchema } from "~/utils/pathBuilder"; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); - const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); - - const presenter = new IntegrationClientConnectionsPresenter(); - const { connections } = await presenter.call({ - userId: userId, - organizationSlug, - clientSlug: clientParam, - }); - - return typedjson({ connections }); -}; - -export default function Page() { - const { connections } = useTypedLoaderData(); - - return ( - - - - ID - Type - Run count - Account - Expires - Created - Updated - - - - {connections.length > 0 ? ( - connections.map((connection) => { - return ( - - {connection.id} - {connectionType(connection.type)} - {connection.runCount} - {connection.metadata?.account ?? "–"} - - - - {} - {} - - ); - }) - ) : ( - - - No connections - - - )} - -
    - ); -} - -function ExpiresAt({ expiresAt }: { expiresAt: Date | null }) { - if (!expiresAt) return <>–; - - const inPast = expiresAt < new Date(); - - return ( - - - - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.scopes/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.scopes/route.tsx deleted file mode 100644 index 41b3b3249a..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam.scopes/route.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { IntegrationClientScopesPresenter } from "~/presenters/IntegrationClientScopesPresenter.server"; -import { requireUserId } from "~/services/session.server"; -import { IntegrationClientParamSchema } from "~/utils/pathBuilder"; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); - const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); - - const presenter = new IntegrationClientScopesPresenter(); - const { scopes } = await presenter.call({ - userId: userId, - organizationSlug, - clientSlug: clientParam, - }); - - return typedjson({ scopes }); -}; - -export default function Page() { - const { scopes } = useTypedLoaderData(); - - return ( -
      - {scopes.map((scope) => ( -
    • - {scope.name} - {scope.description} -
    • - ))} -
    - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route.tsx deleted file mode 100644 index 1ba642eb9c..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.integrations_.$clientParam/route.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Outlet } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { connectionType } from "~/components/integrations/connectionType"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { ClipboardField } from "~/components/primitives/ClipboardField"; -import { DateTime } from "~/components/primitives/DateTime"; -import { - NavBar, - PageInfoGroup, - PageInfoProperty, - PageInfoRow, - PageTabs, - PageTitle, -} from "~/components/primitives/PageHeader"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { IntegrationClientPresenter } from "~/presenters/IntegrationClientPresenter.server"; -import { requireUserId } from "~/services/session.server"; -import { - IntegrationClientParamSchema, - integrationClientConnectionsPath, - integrationClientPath, - integrationClientScopesPath, - organizationIntegrationsPath, -} from "~/utils/pathBuilder"; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); - const { organizationSlug, clientParam } = IntegrationClientParamSchema.parse(params); - - const presenter = new IntegrationClientPresenter(); - const client = await presenter.call({ - userId, - organizationSlug, - clientSlug: clientParam, - }); - - if (!client) { - throw new Response("Not found", { status: 404 }); - } - - return typedjson({ client }); -}; - -export default function Integrations() { - const { client } = useTypedLoaderData(); - const organization = useOrganization(); - - let tabs = [ - { - label: "Jobs", - to: integrationClientPath(organization, client), - }, - ]; - - if (client.authMethod.type !== "local") { - tabs.push({ - label: "Connections", - to: integrationClientConnectionsPath(organization, client), - }); - tabs.push({ - label: "Scopes", - to: integrationClientScopesPath(organization, client), - }); - } - - return ( - - - - - - -
    -
    - - - } - /> - - - - - - } - /> - - - -
    - - -
    -
    -
    - ); -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx index cf60e81862..2c91928129 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx @@ -1,6 +1,6 @@ import { conform, list, requestIntent, useFieldList, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { LockOpenIcon } from "@heroicons/react/20/solid"; +import { EnvelopeIcon, LockOpenIcon, UserPlusIcon } from "@heroicons/react/20/solid"; import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; @@ -143,7 +143,7 @@ export default function Page() {
    } title="Invite team members" description={`Invite new team members to ${organization.title}.`} /> @@ -172,7 +172,7 @@ export default function Page() { { fieldValues.current[index] = e.target.value; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx deleted file mode 100644 index fa47283bb3..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx +++ /dev/null @@ -1,273 +0,0 @@ -import { ArrowUpIcon } from "@heroicons/react/24/solid"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; -import { FrameworkSelector } from "~/components/frameworks/FrameworkSelector"; -import { JobsTable } from "~/components/jobs/JobsTable"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -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"; -import { NamedIcon } from "~/components/primitives/NamedIcon"; -import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { Switch } from "~/components/primitives/Switch"; -import { TextLink } from "~/components/primitives/TextLink"; -import { useFilterJobs } from "~/hooks/useFilterJobs"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { JobListPresenter } from "~/presenters/JobListPresenter.server"; -import { requireUserId } from "~/services/session.server"; -import { cn } from "~/utils/cn"; -import { ProjectParamSchema, organizationIntegrationsPath } from "~/utils/pathBuilder"; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); - - try { - const presenter = new JobListPresenter(); - const jobs = await presenter.call({ userId, organizationSlug, projectSlug: projectParam }); - - return typedjson({ - jobs, - }); - } catch (error) { - console.error(error); - throw new Response(undefined, { - status: 400, - statusText: "Something went wrong, if this problem persists please contact support.", - }); - } -}; - -export default function Page() { - const organization = useOrganization(); - const project = useProject(); - const { jobs } = useTypedLoaderData(); - const { filterText, setFilterText, filteredItems, onlyActiveJobs, setOnlyActiveJobs } = - useFilterJobs(jobs); - const totalJobs = jobs.length; - const hasJobs = totalJobs > 0; - const activeJobCount = jobs.filter((j) => j.status === "ACTIVE").length; - - return ( - - - - - - - {(open) => ( -
    -
    - {hasJobs ? ( - <> - {jobs.some((j) => j.hasIntegrationsRequiringAction) && ( - - Some of your Jobs have Integrations that have not been configured. - - )} -
    -
    - setFilterText(e.target.value)} - autoFocus - /> - - -
    -
    - - {jobs.length === 1 && - jobs.every((r) => r.lastRun === undefined) && - jobs.every((i) => i.hasIntegrationsRequiringAction === false) && ( - - )} - - ) : ( - - )} -
    - - - -
    - )} -
    -
    -
    - ); -} - -function RunYourJobPrompt() { - return ( -
    - - - Your Job is ready to run! Click it to run it now. - -
    - ); -} - -function ExampleJobs() { - return ( - <> - Video walk-through - - Watch Matt, CEO of Trigger.dev create a GitHub issue reminder in Slack using Trigger.dev. - (10 mins) - -