Files
2026-07-13 13:32:57 +08:00

242 lines
8.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import { CheckIcon } from "@heroicons/react/24/solid";
import { useLocation, useNavigation } from "@remix-run/react";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon";
import { MiddleTruncate } from "~/components/primitives/MiddleTruncate";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { PopoverMenuItem } from "~/components/primitives/Popover";
import { Spinner } from "~/components/primitives/Spinner";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
import { RunTag } from "~/components/runs/v3/RunTag";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import {
type SessionListItem,
type SessionList,
} from "~/presenters/v3/SessionListPresenter.server";
import { v3RunPath, v3RunsPath, v3SessionPath } from "~/utils/pathBuilder";
import {
descriptionForSessionStatus,
SessionStatusCombo,
allSessionStatuses,
} from "./SessionStatus";
type SessionsTableProps = Pick<SessionList, "sessions" | "filters" | "hasFilters"> & {
showTopBorder?: boolean;
stickyHeader?: boolean;
};
export function SessionsTable({
sessions,
hasFilters,
showTopBorder = true,
stickyHeader = false,
}: SessionsTableProps) {
const navigation = useNavigation();
const location = useLocation();
const isLoading =
navigation.state !== "idle" && navigation.location?.pathname === location.pathname;
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
return (
<Table
className="max-h-full overflow-y-auto"
showTopBorder={showTopBorder}
stickyHeader={stickyHeader}
>
<TableHeader>
<TableRow>
<TableHeaderCell>ID</TableHeaderCell>
<TableHeaderCell
tooltip={
<div className="flex flex-col divide-y divide-grid-dimmed">
{allSessionStatuses.map((status) => (
<div
key={status}
className="grid grid-cols-[4rem_1fr] gap-x-3 py-2 first:pt-1 last:pb-1"
>
<div className="mb-0.5 flex items-center gap-1.5 whitespace-nowrap">
<SessionStatusCombo status={status} />
</div>
<Paragraph variant="extra-small" className="text-wrap! text-text-dimmed">
{descriptionForSessionStatus(status)}
</Paragraph>
</div>
))}
</div>
}
disableTooltipHoverableContent
>
Status
</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
<TableHeaderCell>Agent ID</TableHeaderCell>
<TableHeaderCell>Test</TableHeaderCell>
<TableHeaderCell>Tags</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Duration</TableHeaderCell>
<TableHeaderCell>
<span className="sr-only">Actions</span>
</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{sessions.length === 0 ? (
<TableBlankRow colSpan={9}>
<div className="flex items-center justify-center">
<Paragraph className="w-auto">
{hasFilters
? "No sessions match these filters"
: "No sessions in this environment yet"}
</Paragraph>
</div>
</TableBlankRow>
) : (
sessions.map((session) => {
const runPath = session.currentRunFriendlyId
? v3RunPath(organization, project, environment, {
friendlyId: session.currentRunFriendlyId,
})
: undefined;
const displayId = session.externalId ?? session.friendlyId;
const sessionPath = v3SessionPath(organization, project, environment, {
friendlyId: session.friendlyId,
});
const allRunsPath = v3RunsPath(organization, project, environment, {
tags: [`chat:${displayId}`],
});
return (
<TableRow key={session.id}>
<TableCell to={sessionPath} isTabbableCell>
<div className="w-[28ch]">
<MiddleTruncate text={displayId} className="font-mono text-xs" />
</div>
</TableCell>
<TableCell to={sessionPath}>
<SimpleTooltip
content={descriptionForSessionStatus(session.status)}
disableHoverableContent
button={<SessionStatusCombo status={session.status} />}
/>
</TableCell>
<TableCell to={sessionPath}>
<span className="font-mono text-xs">{session.type}</span>
</TableCell>
<TableCell to={sessionPath}>
<div className="w-[24ch]">
<MiddleTruncate text={session.taskIdentifier} className="font-mono text-xs" />
</div>
</TableCell>
<TableCell to={sessionPath}>
<span className="sr-only">{session.isTest ? "Yes" : "No"}</span>
{session.isTest ? (
<CheckIcon
aria-hidden
className="size-4 text-text-dimmed group-hover/table-row:text-text-bright"
/>
) : (
<span aria-hidden className="text-text-dimmed">
</span>
)}
</TableCell>
<TableCell to={sessionPath}>
{session.tags.length > 0 ? (
<div className="flex flex-wrap gap-1">
{session.tags.map((tag) => (
<RunTag key={tag} tag={tag} />
))}
</div>
) : (
<span className="text-text-dimmed"></span>
)}
</TableCell>
<TableCell to={sessionPath}>
<DateTime date={session.createdAt} />
</TableCell>
<TableCell to={sessionPath} className="w-[1%]" actionClassName="pr-0 tabular-nums">
<SessionDuration session={session} />
</TableCell>
<SessionActionsCell runPath={runPath} allRunsPath={allRunsPath} />
</TableRow>
);
})
)}
{isLoading && (
<TableBlankRow
colSpan={9}
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-background-deep/90"
>
<Spinner /> <span className="text-text-dimmed">Loading</span>
</TableBlankRow>
)}
</TableBody>
</Table>
);
}
function SessionDuration({ session }: { session: SessionListItem }) {
// Active sessions tick live; closed/expired sessions freeze at the
// moment they ended (closedAt for explicit closes, expiresAt when the
// TTL ran out without a close call).
const endedAt =
session.status === "CLOSED"
? session.closedAt
: session.status === "EXPIRED"
? session.expiresAt
: undefined;
if (endedAt) {
return (
<>{formatDuration(new Date(session.createdAt), new Date(endedAt), { style: "short" })}</>
);
}
return <LiveTimer startTime={new Date(session.createdAt)} />;
}
function SessionActionsCell({ runPath, allRunsPath }: { runPath?: string; allRunsPath: string }) {
return (
<TableCellMenu
isSticky
popoverContent={
<>
{runPath && (
<PopoverMenuItem
to={runPath}
icon={ArrowRightIcon}
leadingIconClassName="text-runs"
title="View current run"
/>
)}
<PopoverMenuItem
to={allRunsPath}
icon={RunsIconExtraSmall}
leadingIconClassName="text-runs"
title="View all runs"
/>
</>
}
/>
);
}