diff --git a/apps/webapp/app/components/primitives/Table.tsx b/apps/webapp/app/components/primitives/Table.tsx index 1a300b2a1..978153ac0 100644 --- a/apps/webapp/app/components/primitives/Table.tsx +++ b/apps/webapp/app/components/primitives/Table.tsx @@ -253,7 +253,7 @@ export const TableCellMenu = forwardRef< type TableBlankRowProps = { className?: string; colSpan: number; - children: ReactNode; + children?: ReactNode; }; export const TableBlankRow = forwardRef( diff --git a/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx new file mode 100644 index 000000000..97f031d33 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx @@ -0,0 +1,43 @@ +import { StopCircleIcon } from "@heroicons/react/20/solid"; +import { useFetcher } from "@remix-run/react"; +import { Button } from "~/components/primitives/Buttons"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, +} from "~/components/primitives/Dialog"; + +type CancelRunDialogProps = { + runFriendlyId: string; + redirectPath: string; +}; + +export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialogProps) { + const cancelFetcher = useFetcher(); + + return ( + + Cancel this run? + + Canceling a run will stop execution. If you want to run this later you will have to replay + the entire run with the original payload. + + + + + + + + ); +} diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 684bc8b73..fb58765dc 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -1,10 +1,10 @@ import { StopIcon } from "@heroicons/react/24/outline"; -import { CheckIcon } from "@heroicons/react/24/solid"; +import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid"; import { User } from "@trigger.dev/database"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { RunListItem } from "~/presenters/v3/RunListPresenter.server"; -import { v3RunPath } from "~/utils/pathBuilder"; +import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server"; +import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder"; import { EnvironmentLabel } from "../../environments/EnvironmentLabel"; import { DateTime } from "../../primitives/DateTime"; import { Paragraph } from "../../primitives/Paragraph"; @@ -15,16 +15,24 @@ import { TableBody, TableCell, TableCellChevron, + TableCellMenu, TableHeader, TableHeaderCell, TableRow, } from "../../primitives/Table"; import { formatDuration } from "@trigger.dev/core/v3"; import { TaskRunStatusCombo } from "./TaskRunStatus"; +import { useEnvironments } from "~/hooks/useEnvironments"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { StopCircleIcon } from "@heroicons/react/20/solid"; +import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; +import { CancelRunDialog } from "./CancelRunDialog"; +import { useLocation } from "@remix-run/react"; type RunsTableProps = { total: number; hasFilters: boolean; + filters: RunListAppliedFilters; showJob?: boolean; runs: RunListItem[]; isLoading?: boolean; @@ -34,12 +42,14 @@ type RunsTableProps = { export function TaskRunsTable({ total, hasFilters, + filters, runs, isLoading = false, currentUser, }: RunsTableProps) { const organization = useOrganization(); const project = useProject(); + const location = useLocation(); return ( @@ -65,9 +75,7 @@ export function TaskRunsTable({ {!isLoading && } ) : runs.length === 0 ? ( - - {!isLoading && } - + ) : ( runs.map((run) => { const path = v3RunPath(organization, project, run); @@ -102,7 +110,23 @@ export function TaskRunsTable({ {run.createdAt ? : "–"} - + {run.isCancellable ? ( + + + + + + + + + ) : ( + {""} + )} ); }) @@ -127,3 +151,62 @@ function NoRuns({ title }: { title: string }) { ); } + +function BlankState({ isLoading, filters }: Pick) { + const organization = useOrganization(); + const project = useProject(); + const envs = useEnvironments(); + if (isLoading) return ; + + const { environments, tasks, from, to, ...otherFilters } = filters; + + if ( + filters.environments.length === 1 && + filters.tasks.length === 1 && + filters.from === undefined && + filters.to === undefined && + Object.values(otherFilters).every((filterArray) => filterArray.length === 0) + ) { + const environment = envs?.find((env) => env.id === filters.environments[0]); + return ( + +
+ + There are no runs for {filters.tasks[0]} + {environment ? ( + <> + {" "} + in + + ) : null} + +
+ + Create a test run + + or + + Triggering a task docs + +
+
+
+ ); + } + + return ( + + + + ); +} diff --git a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts index 37da01f31..144fe2e1c 100644 --- a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts @@ -2,6 +2,7 @@ import { Prisma, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/databa import { Direction } from "~/components/runs/RunStatuses"; import { PrismaClient, prisma } from "~/db.server"; import { getUsername } from "~/utils/username"; +import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server"; type RunListOptions = { userId: string; @@ -23,6 +24,7 @@ const DEFAULT_PAGE_SIZE = 20; export type RunList = Awaited>; export type RunListItem = RunList["runs"][0]; +export type RunListAppliedFilters = RunList["filters"]; export class RunListPresenter { #prismaClient: PrismaClient; @@ -220,6 +222,7 @@ export class RunListPresenter { version: run.version, taskIdentifier: run.taskIdentifier, attempts: Number(run.attempts), + isCancellable: CANCELLABLE_STATUSES.includes(run.status), environment: { type: environment.type, slug: environment.slug, @@ -233,6 +236,14 @@ export class RunListPresenter { previous, }, possibleTasks: possibleTasks.map((task) => task.slug), + filters: { + tasks: tasks || [], + versions: versions || [], + statuses: statuses || [], + environments: environments || [], + from, + to, + }, hasFilters, }; } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx index 7ababa99e..aa2c753b7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx @@ -19,6 +19,7 @@ import { import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Property, PropertyTable } from "~/components/primitives/PropertyTable"; +import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; import { RunIcon } from "~/components/runs/v3/RunIcon"; import { SpanEvents } from "~/components/runs/v3/SpanEvents"; @@ -54,7 +55,6 @@ export default function Page() { const organization = useOrganization(); const project = useProject(); const { runParam } = useParams(); - const cancelFetcher = useFetcher(); return (
- - Cancel this run? - - Canceling a run will stop execution. If you want to run this later you will have - to replay the entire run with the original payload. - - - - - - - + )}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx index 733b941b2..4ec9abbdb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx @@ -581,11 +581,7 @@ function NodeStatusIcon({ node }: { node: RunEvent }) { } function TaskLine({ isError, isSelected }: { isError: boolean; isSelected: boolean }) { - return ( -
- ); + return
; } function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx index 34fcdd1be..a512648dc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx @@ -84,6 +84,7 @@ export default function Page() { = [ +export const CANCELLABLE_STATUSES: Array = [ "PENDING", "EXECUTING", "PAUSED", diff --git a/packages/core/src/v3/index.ts b/packages/core/src/v3/index.ts index 14cb25e81..3f6c31abc 100644 --- a/packages/core/src/v3/index.ts +++ b/packages/core/src/v3/index.ts @@ -42,7 +42,7 @@ export { ConsoleInterceptor } from "./consoleInterceptor"; export { flattenAttributes, unflattenAttributes, - flattenAndNormalizeAttributes, + primitiveValueOrflattenedAttributes, } from "./utils/flattenAttributes"; export { defaultRetryOptions, calculateNextRetryDelay, calculateResetAt } from "./utils/retries"; export { accessoryAttributes } from "./utils/styleAttributes"; diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index 621f236b1..00e6e815e 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -95,13 +95,27 @@ export function unflattenAttributes(obj: Attributes): Record { return result; } -export function flattenAndNormalizeAttributes( +export function primitiveValueOrflattenedAttributes( obj: Record | Array | string | boolean | number | undefined, - prefix: string -): Attributes { + prefix: string | undefined +): Attributes | string | number | boolean | undefined { + if ( + typeof obj === "string" || + typeof obj === "number" || + typeof obj === "boolean" || + obj === null || + obj === undefined + ) { + return obj; + } + const attributes = flattenAttributes(obj, prefix); - if (typeof attributes[prefix] !== "undefined" && attributes[prefix] !== null) { + if ( + prefix !== undefined && + typeof attributes[prefix] !== "undefined" && + attributes[prefix] !== null + ) { return attributes[prefix] as unknown as Attributes; }