diff --git a/apps/webapp/app/assets/icons/MoveToTopIcon.tsx b/apps/webapp/app/assets/icons/MoveToTopIcon.tsx new file mode 100644 index 0000000000..46938fd391 --- /dev/null +++ b/apps/webapp/app/assets/icons/MoveToTopIcon.tsx @@ -0,0 +1,34 @@ +export function MoveToTopIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + ); +} diff --git a/apps/webapp/app/assets/icons/MoveUpIcon.tsx b/apps/webapp/app/assets/icons/MoveUpIcon.tsx new file mode 100644 index 0000000000..6e5d8a84ba --- /dev/null +++ b/apps/webapp/app/assets/icons/MoveUpIcon.tsx @@ -0,0 +1,41 @@ +export function MoveUpIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/apps/webapp/app/components/Shortcuts.tsx b/apps/webapp/app/components/Shortcuts.tsx index 8349ed970f..bf64b4c5f3 100644 --- a/apps/webapp/app/components/Shortcuts.tsx +++ b/apps/webapp/app/components/Shortcuts.tsx @@ -147,6 +147,12 @@ function ShortcutContent() { + + + + + +
Schedules page diff --git a/apps/webapp/app/components/primitives/TextLink.tsx b/apps/webapp/app/components/primitives/TextLink.tsx index 38fd1525c5..d0186268c0 100644 --- a/apps/webapp/app/components/primitives/TextLink.tsx +++ b/apps/webapp/app/components/primitives/TextLink.tsx @@ -1,6 +1,10 @@ import { Link } from "@remix-run/react"; import { cn } from "~/utils/cn"; import { Icon, type RenderIcon } from "./Icon"; +import { useRef } from "react"; +import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { ShortcutKey } from "./ShortcutKey"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip"; const variations = { primary: @@ -17,6 +21,9 @@ type TextLinkProps = { trailingIconClassName?: string; variant?: keyof typeof variations; children: React.ReactNode; + shortcut?: ShortcutDefinition; + hideShortcutKey?: boolean; + tooltip?: React.ReactNode; } & React.AnchorHTMLAttributes; export function TextLink({ @@ -27,20 +34,61 @@ export function TextLink({ trailingIcon, trailingIconClassName, variant = "primary", + shortcut, + hideShortcutKey, + tooltip, ...props }: TextLinkProps) { + const innerRef = useRef(null); const classes = variations[variant]; - return to ? ( - + + if (shortcut) { + useShortcutKeys({ + shortcut: shortcut, + action: () => { + if (innerRef.current) { + innerRef.current.click(); + } + }, + }); + } + + const renderShortcutKey = () => + shortcut && + !hideShortcutKey && ; + + const linkContent = ( + <> {children}{" "} {trailingIcon && } + {shortcut && !tooltip && renderShortcutKey()} + + ); + + const linkElement = to ? ( + + {linkContent} ) : href ? ( - - {children}{" "} - {trailingIcon && } + + {linkContent} ) : ( Need to define a path or href ); + + if (tooltip) { + return ( + + + {linkElement} + + {tooltip} {shortcut && renderShortcutKey()} + + + + ); + } + + return linkElement; } diff --git a/apps/webapp/app/presenters/v3/RunPresenter.server.ts b/apps/webapp/app/presenters/v3/RunPresenter.server.ts index d3ea209924..8ece0dfc3d 100644 --- a/apps/webapp/app/presenters/v3/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunPresenter.server.ts @@ -63,6 +63,14 @@ export class RunPresenter { createdAt: true, }, }, + parentTaskRun: { + select: { + friendlyId: true, + taskIdentifier: true, + spanId: true, + createdAt: true, + }, + }, runtimeEnvironment: { select: { id: true, @@ -111,6 +119,7 @@ export class RunPresenter { completedAt: run.completedAt, logsDeletedAt: showDeletedLogs ? null : run.logsDeletedAt, rootTaskRun: run.rootTaskRun, + parentTaskRun: run.parentTaskRun, environment: { id: run.runtimeEnvironment.id, organizationId: run.runtimeEnvironment.organizationId, @@ -202,8 +211,6 @@ export class RunPresenter { trace: { rootSpanStatus, events: events, - parentRunFriendlyId: - tree?.id === traceSummary.rootSpan.id ? undefined : traceSummary.rootSpan.runId, duration: totalDuration, rootStartedAt: tree?.data.startTime, startedAt: run.startedAt, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 9e9145be9a..e3b4ea1971 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -11,7 +11,7 @@ import { MagnifyingGlassPlusIcon, StopCircleIcon, } from "@heroicons/react/20/solid"; -import { useLoaderData, useParams, useRevalidator } from "@remix-run/react"; +import { useLoaderData, useRevalidator } from "@remix-run/react"; import { type LoaderFunctionArgs, type SerializeFrom, json } from "@remix-run/server-runtime"; import { type Virtualizer } from "@tanstack/react-virtual"; import { @@ -25,7 +25,8 @@ import { motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { redirect } from "remix-typedjson"; -import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParentIcon"; +import { MoveToTopIcon } from "~/assets/icons/MoveToTopIcon"; +import { MoveUpIcon } from "~/assets/icons/MoveUpIcon"; import tileBgPath from "~/assets/images/error-banner-tile@2x.png"; import { DevDisconnectedBanner, useCrossEngineIsConnected } from "~/components/DevPresence"; import { WarmStartIconWithTooltip } from "~/components/WarmStarts"; @@ -87,7 +88,6 @@ import { docsPath, v3BillingPath, v3RunParamsSchema, - v3RunPath, v3RunRedirectPath, v3RunSpanPath, v3RunStreamingPath, @@ -301,8 +301,7 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade return <>; } - const { events, parentRunFriendlyId, duration, rootSpanStatus, rootStartedAt, queuedDuration } = - trace; + const { events, duration, rootSpanStatus, rootStartedAt, queuedDuration } = trace; const shouldLiveReload = events.length <= maximumLiveReloadingSetting; const changeToSpan = useDebounce((selectedSpan: string) => { @@ -339,7 +338,6 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade selectedId={selectedSpanId} key={events[0]?.id ?? "-"} events={events} - parentRunFriendlyId={parentRunFriendlyId} onSelectedIdChanged={(selectedSpan) => { //instantly close the panel if no span is selected if (!selectedSpan) { @@ -357,6 +355,7 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade shouldLiveReload={shouldLiveReload} maximumLiveReloadingSetting={maximumLiveReloadingSetting} rootRun={run.rootTaskRun} + parentRun={run.parentTaskRun} isCompleted={run.completedAt !== null} /> @@ -475,7 +474,6 @@ function NoLogsView({ run, resizable }: LoaderData) { type TasksTreeViewProps = { events: TraceEvent[]; selectedId?: string; - parentRunFriendlyId?: string; onSelectedIdChanged: (selectedId: string | undefined) => void; totalDuration: number; rootSpanStatus: "executing" | "completed" | "failed"; @@ -489,13 +487,17 @@ type TasksTreeViewProps = { taskIdentifier: string; spanId: string; } | null; + parentRun: { + friendlyId: string; + taskIdentifier: string; + spanId: string; + } | null; isCompleted: boolean; }; function TasksTreeView({ events, selectedId, - parentRunFriendlyId, onSelectedIdChanged, totalDuration, rootSpanStatus, @@ -505,6 +507,7 @@ function TasksTreeView({ shouldLiveReload, maximumLiveReloadingSetting, rootRun, + parentRun, isCompleted, }: TasksTreeViewProps) { const isAdmin = useHasAdminAccess(); @@ -595,20 +598,32 @@ function TasksTreeView({ id={resizableSettings.tree.tree.id} default={resizableSettings.tree.tree.default} min={resizableSettings.tree.tree.min} - className="pl-3" >
-
- {rootRun ? ( - + {rootRun || parentRun ? ( + - ) : parentRunFriendlyId ? ( - ) : ( - + This is the root task )} @@ -627,6 +642,7 @@ function TasksTreeView({ nodes={nodes} getNodeProps={getNodeProps} getTreeProps={getTreeProps} + parentClassName="pl-3" renderNode={({ node, state, index }) => ( <>
; } -function ShowParentLink({ - runFriendlyId, - spanId, - isRoot, +function ShowParentOrRootLinks({ + relationships, }: { - runFriendlyId: string; - spanId?: string; - isRoot: boolean; + relationships: { + root?: { + friendlyId: string; + taskIdentifier: string; + spanId: string; + isParent?: boolean; + }; + parent?: { + friendlyId: string; + taskIdentifier: string; + spanId: string; + }; + }; }) { - const [mouseOver, setMouseOver] = useState(false); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); - const { spanParam } = useParams(); - const span = spanId ? spanId : spanParam; + // Case 1: Root is also the parent + if (relationships.root?.isParent === true) { + return ( + + Jump to root and parent run + +
+ } + className="text-xs" + > + Root/parent + + ); + } + // Case 2: Root and Parent are different runs return ( - setMouseOver(true)} - onMouseLeave={() => setMouseOver(false)} - fullWidth - textAlignLeft - shortcut={{ key: "p" }} - className="flex-1" - > - {mouseOver ? ( - - ) : ( - +
+ {relationships.root && ( + + Jump to root run + +
+ } + className="text-xs" + > + Root +
)} - - {isRoot ? "Show root run" : "Show parent run"} - - + {relationships.parent && ( + + Jump to parent run + +
+ } + className="text-xs" + > + Parent + + )} +
); } diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index 4871a2d0b7..98ffecb50b 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -7,16 +7,16 @@ import { } from "@heroicons/react/20/solid"; import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { Form, useLocation, useNavigation } from "@remix-run/react"; -import { ActionFunctionArgs } from "@remix-run/server-runtime"; +import { type ActionFunctionArgs } from "@remix-run/server-runtime"; import { uiComponent } from "@team-plain/typescript-sdk"; import { GitHubLightIcon } from "@trigger.dev/companyicons"; import { - FreePlanDefinition, - Limits, - PaidPlanDefinition, - Plans, - SetPlanBody, - SubscriptionResult, + type FreePlanDefinition, + type Limits, + type PaidPlanDefinition, + type Plans, + type SetPlanBody, + type SubscriptionResult, } from "@trigger.dev/platform/v3"; import React, { useEffect, useState } from "react"; import { z } from "zod"; @@ -168,6 +168,10 @@ const pricingDefinitions = { title: "Concurrent runs", content: "The number of runs that can be executed at the same time.", }, + additionalConcurrency: { + title: "Additional concurrency", + content: "Then $50/month per 50", + }, taskRun: { title: "Task runs", content: "A single execution of a task.", @@ -185,6 +189,10 @@ const pricingDefinitions = { title: "Schedules", content: "You can attach recurring schedules to tasks using cron syntax.", }, + additionalSchedules: { + title: "Additional schedules", + content: "Then $10/month per 1,000", + }, alerts: { title: "Alert destination", content: @@ -195,9 +203,22 @@ const pricingDefinitions = { content: "Realtime allows you to send the live status and data from your runs to your frontend. This is the number of simultaneous Realtime connections that can be made.", }, + additionalRealtimeConnections: { + title: "Additional Realtime connections", + content: "Then $10/month per 100", + }, + additionalSeats: { + title: "Additional seats", + content: "Then $20/month per seat", + }, branches: { title: "Branches", - content: "The number of preview branches that can be active (you can archive old ones).", + content: + "Preview branches allow you to test changes before deploying to production. You can have a limited number active at once (but can archive old ones).", + }, + additionalBranches: { + title: "Additional branches", + content: "Then $10/month per branch", }, }; @@ -381,7 +402,7 @@ export function TierFree({
-
@@ -504,7 +525,7 @@ export function TierFree({ - + )} @@ -581,7 +602,7 @@ export function TierHobby({
) : (