Skip to content

Task and run page improvements #1076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ build-storybook.log
.storybook-out
storybook-static

/prisma/seed.js
/prisma/seed.js
/prisma/populate.js
103 changes: 103 additions & 0 deletions apps/webapp/app/components/primitives/TooltipPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { VirtualElement as IVirtualElement } from "@popperjs/core";
import { ReactNode, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { useEvent } from "react-use";
import useLazyRef from "~/hooks/useLazyRef";

// Recharts 3.x will have portal support, but until then we're using this:
//https://github.com/recharts/recharts/issues/2458#issuecomment-1063463873

export interface PopperPortalProps {
active?: boolean;
children: ReactNode;
}

export default function TooltipPortal({ active = true, children }: PopperPortalProps) {
const [portalElement, setPortalElement] = useState<HTMLDivElement>();
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>();
const virtualElementRef = useLazyRef(() => new VirtualElement());

const { styles, attributes, update } = usePopper(
virtualElementRef.current,
popperElement,
POPPER_OPTIONS
);

useEffect(() => {
const el = document.createElement("div");
document.body.appendChild(el);
setPortalElement(el);
return () => el.remove();
}, []);

useEvent("mousemove", ({ clientX: x, clientY: y }) => {
virtualElementRef.current?.update(x, y);
if (!active) return;
update?.();
});

useEffect(() => {
if (!active) return;
update?.();
}, [active, update]);

if (!portalElement) return null;

return createPortal(
<div
ref={setPopperElement}
{...attributes.popper}
style={{
...styles.popper,
zIndex: 1000,
display: active ? "block" : "none",
}}
>
{children}
</div>,
portalElement
);
}

class VirtualElement implements IVirtualElement {
private rect = {
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
x: 0,
y: 0,
toJSON() {
return this;
},
};

update(x: number, y: number) {
this.rect.y = y;
this.rect.top = y;
this.rect.bottom = y;

this.rect.x = x;
this.rect.left = x;
this.rect.right = x;
}

getBoundingClientRect(): DOMRect {
return this.rect;
}
}

const POPPER_OPTIONS: Parameters<typeof usePopper>[2] = {
placement: "right-start",
modifiers: [
{
name: "offset",
options: {
offset: [8, 8],
},
},
],
};
10 changes: 3 additions & 7 deletions apps/webapp/app/components/runs/v3/LiveTimer.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { formatDuration } from "@trigger.dev/core/v3";
import { useState, useEffect } from "react";
import { Paragraph } from "~/components/primitives/Paragraph";
import { cn } from "~/utils/cn";
import { useEffect, useState } from "react";

export function LiveTimer({
startTime,
endTime,
updateInterval = 250,
className,
}: {
startTime: Date;
endTime?: Date;
updateInterval?: number;
className?: string;
}) {
const [now, setNow] = useState<Date>();

Expand All @@ -30,13 +26,13 @@ export function LiveTimer({
}, [startTime]);

return (
<Paragraph variant="extra-small" className={cn("whitespace-nowrap tabular-nums", className)}>
<>
{formatDuration(startTime, now, {
style: "short",
maxDecimalPoints: 0,
units: ["d", "h", "m", "s"],
})}
</Paragraph>
</>
);
}

Expand Down
9 changes: 9 additions & 0 deletions apps/webapp/app/components/runs/v3/TaskRunStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export const RUNNING_STATUSES: TaskRunStatus[] = [
"WAITING_TO_RESUME",
];

export const FINISHED_STATUSES: TaskRunStatus[] = [
"COMPLETED_SUCCESSFULLY",
"CANCELED",
"COMPLETED_WITH_ERRORS",
"INTERRUPTED",
"SYSTEM_FAILURE",
"CRASHED",
];

export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
return taskRunStatusDescriptions[status];
}
Expand Down
14 changes: 10 additions & 4 deletions apps/webapp/app/components/runs/v3/TaskRunsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { StopIcon } from "@heroicons/react/24/outline";
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
import { useLocation } from "@remix-run/react";
import { formatDuration } from "@trigger.dev/core/v3";
import { User } from "@trigger.dev/database";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { useEnvironments } from "~/hooks/useEnvironments";
Expand All @@ -28,6 +27,7 @@ import {
import { CancelRunDialog } from "./CancelRunDialog";
import { ReplayRunDialog } from "./ReplayRunDialog";
import { TaskRunStatusCombo } from "./TaskRunStatus";
import { LiveTimer } from "./LiveTimer";

type RunsTableProps = {
total: number;
Expand Down Expand Up @@ -94,9 +94,15 @@ export function TaskRunsTable({
{run.startedAt ? <DateTime date={run.startedAt} /> : "–"}
</TableCell>
<TableCell to={path}>
{formatDuration(run.startedAt, run.completedAt, {
style: "short",
})}
{run.startedAt && run.finishedAt ? (
formatDuration(new Date(run.startedAt), new Date(run.finishedAt), {
style: "short",
})
) : run.startedAt ? (
<LiveTimer startTime={new Date(run.startedAt)} />
) : (
"–"
)}
</TableCell>
<TableCell to={path}>
{run.isTest ? (
Expand Down
11 changes: 11 additions & 0 deletions apps/webapp/app/hooks/useLazyRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useRef, MutableRefObject } from "react";

const useLazyRef = <T>(initialValFunc: () => T) => {
const ref: MutableRefObject<T | null> = useRef(null);
if (ref.current === null) {
ref.current = initialValFunc();
}
return ref;
};

export default useLazyRef;
53 changes: 18 additions & 35 deletions apps/webapp/app/presenters/v3/RunListPresenter.server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
import { Direction } from "~/components/runs/RunStatuses";
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { sqlDatabaseSchema } from "~/db.server";
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
import { getUsername } from "~/utils/username";
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
import { BasePresenter } from "./basePresenter.server";

type RunListOptions = {
userId?: string;
Expand All @@ -28,13 +29,7 @@ export type RunList = Awaited<ReturnType<RunListPresenter["call"]>>;
export type RunListItem = RunList["runs"][0];
export type RunListAppliedFilters = RunList["filters"];

export class RunListPresenter {
#prismaClient: PrismaClient;

constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}

export class RunListPresenter extends BasePresenter {
public async call({
userId,
projectSlug,
Expand All @@ -60,7 +55,7 @@ export class RunListPresenter {
to !== undefined;

// Find the project scoped to the organization
const project = await this.#prismaClient.project.findFirstOrThrow({
const project = await this._replica.project.findFirstOrThrow({
select: {
id: true,
environments: {
Expand Down Expand Up @@ -88,15 +83,15 @@ export class RunListPresenter {
});

//get all possible tasks
const possibleTasks = await this.#prismaClient.backgroundWorkerTask.findMany({
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
distinct: ["slug"],
where: {
projectId: project.id,
},
});

//get the runs
let runs = await this.#prismaClient.$queryRaw<
let runs = await this._replica.$queryRaw<
{
id: string;
number: BigInt;
Expand All @@ -107,10 +102,9 @@ export class RunListPresenter {
status: TaskRunStatus;
createdAt: Date;
lockedAt: Date | null;
completedAt: Date | null;
updatedAt: Date;
isTest: boolean;
spanId: string;
attempts: BigInt;
}[]
>`
SELECT
Expand All @@ -123,20 +117,13 @@ export class RunListPresenter {
tr.status AS status,
tr."createdAt" AS "createdAt",
tr."lockedAt" AS "lockedAt",
tra."completedAt" AS "completedAt",
tr."updatedAt" AS "updatedAt",
tr."isTest" AS "isTest",
tr."spanId" AS "spanId",
COUNT(tra.id) AS attempts
tr."spanId" AS "spanId"
FROM
${sqlDatabaseSchema}."TaskRun" tr
LEFT JOIN
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY "taskRunId" ORDER BY "createdAt" DESC) rn
FROM ${sqlDatabaseSchema}."TaskRunAttempt"
) tra ON tr.id = tra."taskRunId" AND tra.rn = 1
LEFT JOIN
${sqlDatabaseSchema}."BackgroundWorker" bw ON tra."backgroundWorkerId" = bw.id
${sqlDatabaseSchema}."BackgroundWorker" bw ON tr."lockedToVersionId" = bw.id
WHERE
-- project
tr."projectId" = ${project.id}
Expand All @@ -154,15 +141,11 @@ export class RunListPresenter {
? Prisma.sql`AND tr."taskIdentifier" IN (${Prisma.join(tasks)})`
: Prisma.empty
}
${hasStatusFilters ? Prisma.sql`AND (` : Prisma.empty}
${
statuses && statuses.length > 0
? Prisma.sql`tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
? Prisma.sql`AND tr.status = ANY(ARRAY[${Prisma.join(statuses)}]::"TaskRunStatus"[])`
: Prisma.empty
}
${statuses && statuses.length > 0 && hasStatusFilters ? Prisma.sql` OR ` : Prisma.empty}
${hasStatusFilters ? Prisma.sql`tr.status IS NULL` : Prisma.empty}
${hasStatusFilters ? Prisma.sql`) ` : Prisma.empty}
${
environments && environments.length > 0
? Prisma.sql`AND tr."runtimeEnvironmentId" IN (${Prisma.join(environments)})`
Expand All @@ -179,8 +162,6 @@ export class RunListPresenter {
? Prisma.sql`AND tr."createdAt" <= ${new Date(to).toISOString()}::timestamp`
: Prisma.empty
}
GROUP BY
tr."friendlyId", tr."taskIdentifier", tr."runtimeEnvironmentId", tr.id, bw.version, tra.status, tr."createdAt", tra."startedAt", tra."completedAt"
ORDER BY
${direction === "forward" ? Prisma.sql`tr.id DESC` : Prisma.sql`tr.id ASC`}
LIMIT ${pageSize + 1}`;
Expand Down Expand Up @@ -219,19 +200,21 @@ export class RunListPresenter {
throw new Error(`Environment not found for TaskRun ${run.id}`);
}

const hasFinished = FINISHED_STATUSES.includes(run.status);

return {
id: run.id,
friendlyId: run.runFriendlyId,
number: Number(run.number),
createdAt: run.createdAt,
startedAt: run.lockedAt,
completedAt: run.completedAt,
createdAt: run.createdAt.toISOString(),
startedAt: run.lockedAt ? run.lockedAt.toISOString() : undefined,
hasFinished,
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
isTest: run.isTest,
status: run.status,
version: run.version,
taskIdentifier: run.taskIdentifier,
spanId: run.spanId,
attempts: Number(run.attempts),
isReplayable: true,
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
environment: displayableEnvironments(environment, userId),
Expand Down
Loading