Merge remote-tracking branch 'origin/main' into bulk-cancel-replay

This commit is contained in:
Matt Aitken
2025-06-16 13:32:44 +01:00
11 changed files with 151 additions and 53 deletions
@@ -4,9 +4,17 @@ import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useCopy } from "~/hooks/useCopy";
import { cn } from "~/utils/cn";
export function CopyableText({ value, className }: { value: string; className?: string }) {
export function CopyableText({
value,
copyValue,
className,
}: {
value: string;
copyValue?: string;
className?: string;
}) {
const [isHovered, setIsHovered] = useState(false);
const { copy, copied } = useCopy(value);
const { copy, copied } = useCopy(copyValue ?? value);
return (
<span
@@ -52,6 +52,8 @@ import {
TaskRunStatusCombo,
} from "./TaskRunStatus";
import { useEnvironment } from "~/hooks/useEnvironment";
import { CopyableText } from "~/components/primitives/CopyableText";
import { ClipboardField } from "~/components/primitives/ClipboardField";
type RunsTableProps = {
total: number;
@@ -134,7 +136,7 @@ export function TaskRunsTable({
)}
</TableHeaderCell>
)}
<TableHeaderCell alignment="right">Run #</TableHeaderCell>
<TableHeaderCell>ID</TableHeaderCell>
<TableHeaderCell>Task</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell
@@ -306,8 +308,21 @@ export function TaskRunsTable({
/>
</TableCell>
)}
<TableCell to={path} alignment="right" isTabbableCell>
{formatNumber(run.number)}
<TableCell to={path} isTabbableCell>
<SimpleTooltip
content={run.friendlyId}
button={
<span className="flex h-6 items-center gap-1">
<CopyableText
value={run.friendlyId.slice(-8)}
copyValue={run.friendlyId}
className="font-mono"
/>
</span>
}
asChild
disableHoverableContent
/>
</TableCell>
<TableCell to={path}>
<span className="flex items-center gap-x-1">
@@ -45,6 +45,7 @@ export class NextRunListPresenter {
) {}
public async call(
organizationId: string,
environmentId: string,
{
userId,
@@ -190,6 +191,7 @@ export class NextRunListPresenter {
});
const { runs, pagination } = await runsRepository.listRuns({
organizationId,
environmentId,
projectId,
tasks,
@@ -1,16 +1,16 @@
import {
PrismaClientOrTransaction,
RuntimeEnvironmentType,
type PrismaClientOrTransaction,
type RuntimeEnvironmentType,
type TaskTriggerSource,
} from "@trigger.dev/database";
import { $replica } from "~/db.server";
import { clickhouseClient } from "~/services/clickhouseInstance.server";
import {
AverageDurations,
type AverageDurations,
ClickHouseEnvironmentMetricsRepository,
CurrentRunningStats,
DailyTaskActivity,
EnvironmentMetricsRepository,
type CurrentRunningStats,
type DailyTaskActivity,
type EnvironmentMetricsRepository,
PostgrestEnvironmentMetricsRepository,
} from "~/services/environmentMetricsRepository.server";
import { singleton } from "~/utils/singleton";
@@ -32,9 +32,13 @@ export class TaskListPresenter {
) {}
public async call({
organizationId,
projectId,
environmentId,
environmentType,
}: {
organizationId: string;
projectId: string;
environmentId: string;
environmentType: RuntimeEnvironmentType;
}) {
@@ -76,18 +80,24 @@ export class TaskListPresenter {
// IMPORTANT: Don't await these, we want to return the promises
// so we can defer the loading of the data
const activity = this.environmentMetricsRepository.getDailyTaskActivity({
organizationId,
projectId,
environmentId,
days: 6, // This actually means 7 days, because we want to show the current day too
tasks: slugs,
});
const runningStats = this.environmentMetricsRepository.getCurrentRunningStats({
organizationId,
projectId,
environmentId,
days: 6,
tasks: slugs,
});
const durations = this.environmentMetricsRepository.getAverageDurations({
organizationId,
projectId,
environmentId,
days: 6,
tasks: slugs,
@@ -125,6 +125,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
try {
const { tasks, activity, runningStats, durations } = await taskListPresenter.call({
organizationId: project.organizationId,
projectId: project.id,
environmentId: environment.id,
environmentType: environment.type,
});
@@ -128,7 +128,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
}
const presenter = new NextRunListPresenter($replica, clickhouseClient);
const list = presenter.call(environment.id, {
const list = presenter.call(project.organizationId, environment.id, {
userId,
projectId: project.id,
tasks,
@@ -10,18 +10,24 @@ export type AverageDurations = Record<string, number>;
export interface EnvironmentMetricsRepository {
getDailyTaskActivity(options: {
organizationId: string;
projectId: string;
environmentId: string;
days: number;
tasks: string[];
}): Promise<DailyTaskActivity>;
getCurrentRunningStats(options: {
organizationId: string;
projectId: string;
environmentId: string;
days: number;
tasks: string[];
}): Promise<CurrentRunningStats>;
getAverageDurations(options: {
organizationId: string;
projectId: string;
environmentId: string;
days: number;
tasks: string[];
@@ -177,10 +183,14 @@ export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetric
constructor(private readonly options: ClickHouseEnvironmentMetricsRepositoryOptions) {}
public async getDailyTaskActivity({
organizationId,
projectId,
environmentId,
days,
tasks,
}: {
organizationId: string;
projectId: string;
environmentId: string;
days: number;
tasks: string[];
@@ -190,6 +200,8 @@ export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetric
}
const [queryError, activity] = await this.options.clickhouse.taskRuns.getTaskActivity({
organizationId,
projectId,
environmentId,
days,
});
@@ -210,10 +222,14 @@ export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetric
}
public async getCurrentRunningStats({
organizationId,
projectId,
environmentId,
days,
tasks,
}: {
organizationId: string;
projectId: string;
environmentId: string;
days: number;
tasks: string[];
@@ -223,6 +239,8 @@ export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetric
}
const [queryError, stats] = await this.options.clickhouse.taskRuns.getCurrentRunningStats({
organizationId,
projectId,
environmentId,
days,
});
@@ -242,10 +260,14 @@ export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetric
}
public async getAverageDurations({
organizationId,
projectId,
environmentId,
days,
tasks,
}: {
organizationId: string;
projectId: string;
environmentId: string;
days: number;
tasks: string[];
@@ -255,6 +277,8 @@ export class ClickHouseEnvironmentMetricsRepository implements EnvironmentMetric
}
const [queryError, durations] = await this.options.clickhouse.taskRuns.getAverageDurations({
organizationId,
projectId,
environmentId,
days,
});
@@ -1,8 +1,8 @@
import { ClickHouse } from "@internal/clickhouse";
import { Tracer } from "@internal/tracing";
import { Logger, LogLevel } from "@trigger.dev/core/logger";
import { TaskRunStatus } from "@trigger.dev/database";
import { PrismaClient } from "~/db.server";
import { type ClickHouse } from "@internal/clickhouse";
import { type Tracer } from "@internal/tracing";
import { type Logger, type LogLevel } from "@trigger.dev/core/logger";
import { type TaskRunStatus } from "@trigger.dev/database";
import { type PrismaClient } from "~/db.server";
export type RunsRepositoryOptions = {
clickhouse: ClickHouse;
@@ -13,6 +13,7 @@ export type RunsRepositoryOptions = {
};
export type ListRunsOptions = {
organizationId: string;
projectId: string;
environmentId: string;
//filters
@@ -43,11 +44,14 @@ export class RunsRepository {
async listRuns(options: ListRunsOptions) {
const queryBuilder = this.options.clickhouse.taskRuns.queryBuilder();
queryBuilder
.where("environment_id = {environmentId: String}", {
environmentId: options.environmentId,
.where("organization_id = {organizationId: String}", {
organizationId: options.organizationId,
})
.where("project_id = {projectId: String}", {
projectId: options.projectId,
})
.where("environment_id = {environmentId: String}", {
environmentId: options.environmentId,
});
if (options.tasks && options.tasks.length > 0) {
@@ -115,17 +119,17 @@ export class RunsRepository {
if (options.page.direction === "forward") {
queryBuilder
.where("run_id < {runId: String}", { runId: options.page.cursor })
.orderBy("run_id DESC")
.orderBy("created_at DESC, run_id DESC")
.limit(options.page.size + 1);
} else {
queryBuilder
.where("run_id > {runId: String}", { runId: options.page.cursor })
.orderBy("run_id DESC")
.orderBy("created_at ASC, run_id ASC")
.limit(options.page.size + 1);
}
} else {
// Initial page - no cursor provided
queryBuilder.orderBy("run_id DESC").limit(options.page.size + 1);
queryBuilder.orderBy("created_at DESC, run_id DESC").limit(options.page.size + 1);
}
const [queryError, result] = await queryBuilder.execute();
@@ -143,38 +147,33 @@ export class RunsRepository {
let previousCursor: string | null = null;
//get cursors for next and previous pages
if (options.page.cursor) {
switch (options.page.direction) {
case "forward":
previousCursor = runIds.at(0) ?? null;
if (hasMore) {
// The next cursor should be the last run ID from this page
nextCursor = runIds[options.page.size - 1];
}
break;
case "backward":
// No need to reverse since we're using DESC ordering consistently
if (hasMore) {
previousCursor = runIds[options.page.size - 1];
}
nextCursor = runIds.at(0) ?? null;
break;
default:
// This shouldn't happen if cursor is provided, but handle it
if (hasMore) {
nextCursor = runIds[options.page.size - 1];
}
break;
const direction = options.page.direction ?? "forward";
switch (direction) {
case "forward": {
previousCursor = options.page.cursor ? runIds.at(0) ?? null : null;
if (hasMore) {
// The next cursor should be the last run ID from this page
nextCursor = runIds[options.page.size - 1];
}
break;
}
} else {
// Initial page - no cursor
if (hasMore) {
// The next cursor should be the last run ID from this page
nextCursor = runIds[options.page.size - 1];
case "backward": {
const reversedRunIds = [...runIds].reverse();
if (hasMore) {
previousCursor = reversedRunIds.at(1) ?? null;
nextCursor = reversedRunIds.at(options.page.size) ?? null;
} else {
nextCursor = reversedRunIds.at(options.page.size - 1) ?? null;
}
break;
}
}
const runIdsToReturn = hasMore ? runIds.slice(0, -1) : runIds;
const runIdsToReturn =
options.page.direction === "backward" && hasMore
? runIds.slice(1, options.page.size + 1)
: runIds.slice(0, options.page.size);
const runs = await this.options.prisma.taskRun.findMany({
where: {
+16
View File
@@ -72,6 +72,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
});
expect(runs).toHaveLength(1);
@@ -180,6 +181,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
tasks: ["task-1", "task-2"],
});
@@ -290,6 +292,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
versions: ["1.0.0", "3.0.0"],
});
@@ -400,6 +403,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
statuses: ["PENDING", "COMPLETED_SUCCESSFULLY"],
});
@@ -510,6 +514,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
tags: ["urgent"],
});
@@ -619,6 +624,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
scheduleId: "schedule_1",
});
@@ -712,6 +718,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
isTest: true,
});
@@ -723,6 +730,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
isTest: false,
});
@@ -816,6 +824,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
rootOnly: true,
});
@@ -945,6 +954,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
batchId: batchRun1.id,
});
@@ -1052,6 +1062,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
runFriendlyIds: ["run_abc", "run_xyz"],
});
@@ -1159,6 +1170,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
runIds: [run1.id, run3.id],
});
@@ -1273,6 +1285,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
from: yesterday.getTime(),
to: now.getTime(),
});
@@ -1393,6 +1406,7 @@ describe("RunsRepository", () => {
page: { size: 10 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
tasks: ["task-1"],
versions: ["1.0.0"],
statuses: ["COMPLETED_SUCCESSFULLY"],
@@ -1476,6 +1490,7 @@ describe("RunsRepository", () => {
page: { size: 2 },
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
});
expect(firstPage.runs).toHaveLength(2);
@@ -1491,6 +1506,7 @@ describe("RunsRepository", () => {
},
projectId: project.id,
environmentId: runtimeEnvironment.id,
organizationId: organization.id,
});
expect(secondPage.runs).toHaveLength(2);
+10
View File
@@ -59,6 +59,16 @@ Here are some common problems and their solutions:
There should be a link below the error message to the full build logs on your machine. Take a look at these to see what went wrong. Join [our Discord](https://trigger.dev/discord) and you share it privately with us if you can't figure out what's going wrong. Do NOT share these publicly as the verbose logs might reveal private information about your project.
### `Error: failed to solve: failed to resolve source metadata for docker.io/docker/dockerfile:1`
If you see this error after uninstalling Docker Desktop:
```
Error: failed to solve: failed to resolve source metadata for docker.io/docker/dockerfile:1: error getting credentials - err: exec: "docker-credential-desktop": executable file not found in $PATH
```
This happens because Docker Desktop left behind a config file that's still trying to use its credential store. To fix this, remove or update the `~/.docker/config.json` file. You don't need Docker Desktop installed to use Trigger.dev.
### `Deployment encountered an error`
Usually there will be some useful guidance below this message. If you can't figure out what's going wrong then join [our Discord](https://trigger.dev/discord) and create a Help forum post with a link to your deployment.
+15 -3
View File
@@ -113,6 +113,8 @@ export const TaskActivityQueryResult = z.object({
export type TaskActivityQueryResult = z.infer<typeof TaskActivityQueryResult>;
export const TaskActivityQueryParams = z.object({
organizationId: z.string(),
projectId: z.string(),
environmentId: z.string(),
days: z.number().int(),
});
@@ -128,7 +130,9 @@ export function getTaskActivityQueryBuilder(ch: ClickhouseReader, settings?: Cli
count() as count
FROM trigger_dev.task_runs_v2 FINAL
WHERE
environment_id = {environmentId: String}
organization_id = {organizationId: String}
AND project_id = {projectId: String}
AND environment_id = {environmentId: String}
AND created_at >= today() - {days: Int64}
AND _is_deleted = 0
GROUP BY
@@ -155,6 +159,8 @@ export const CurrentRunningStatsQueryResult = z.object({
export type CurrentRunningStatsQueryResult = z.infer<typeof CurrentRunningStatsQueryResult>;
export const CurrentRunningStatsQueryParams = z.object({
organizationId: z.string(),
projectId: z.string(),
environmentId: z.string(),
days: z.number().int(),
});
@@ -169,7 +175,9 @@ export function getCurrentRunningStats(ch: ClickhouseReader, settings?: ClickHou
count() as count
FROM trigger_dev.task_runs_v2 FINAL
WHERE
environment_id = {environmentId: String}
organization_id = {organizationId: String}
AND project_id = {projectId: String}
AND environment_id = {environmentId: String}
AND status IN ('PENDING', 'WAITING_FOR_DEPLOY', 'WAITING_TO_RESUME', 'QUEUED', 'EXECUTING')
AND _is_deleted = 0
AND created_at >= now() - INTERVAL {days: Int64} DAY
@@ -193,6 +201,8 @@ export const AverageDurationsQueryResult = z.object({
export type AverageDurationsQueryResult = z.infer<typeof AverageDurationsQueryResult>;
export const AverageDurationsQueryParams = z.object({
organizationId: z.string(),
projectId: z.string(),
environmentId: z.string(),
days: z.number().int(),
});
@@ -206,7 +216,9 @@ export function getAverageDurations(ch: ClickhouseReader, settings?: ClickHouseS
avg(toUnixTimestamp(completed_at) - toUnixTimestamp(started_at)) as duration
FROM trigger_dev.task_runs_v2 FINAL
WHERE
environment_id = {environmentId: String}
organization_id = {organizationId: String}
AND project_id = {projectId: String}
AND environment_id = {environmentId: String}
AND created_at >= today() - {days: Int64}
AND status IN ('COMPLETED_SUCCESSFULLY', 'COMPLETED_WITH_ERRORS')
AND started_at IS NOT NULL