diff --git a/apps/webapp/app/components/primitives/CopyableText.tsx b/apps/webapp/app/components/primitives/CopyableText.tsx
index 6a37c6f2bf..99664b3dc3 100644
--- a/apps/webapp/app/components/primitives/CopyableText.tsx
+++ b/apps/webapp/app/components/primitives/CopyableText.tsx
@@ -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 (
)}
- Run #
+ ID
Task
Version
)}
-
- {formatNumber(run.number)}
+
+
+
+
+ }
+ asChild
+ disableHoverableContent
+ />
diff --git a/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts b/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts
index bee9159016..ec5703b60b 100644
--- a/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts
@@ -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,
diff --git a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts
index 4c8acddb99..59313a41c1 100644
--- a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts
@@ -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,
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx
index d2fbf5794d..6152f8579c 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx
@@ -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,
});
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.next.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.next.runs._index/route.tsx
index d68d8287d1..e73b1c883e 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.next.runs._index/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.next.runs._index/route.tsx
@@ -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,
diff --git a/apps/webapp/app/services/environmentMetricsRepository.server.ts b/apps/webapp/app/services/environmentMetricsRepository.server.ts
index 6b0251b753..2849afbff6 100644
--- a/apps/webapp/app/services/environmentMetricsRepository.server.ts
+++ b/apps/webapp/app/services/environmentMetricsRepository.server.ts
@@ -10,18 +10,24 @@ export type AverageDurations = Record;
export interface EnvironmentMetricsRepository {
getDailyTaskActivity(options: {
+ organizationId: string;
+ projectId: string;
environmentId: string;
days: number;
tasks: string[];
}): Promise;
getCurrentRunningStats(options: {
+ organizationId: string;
+ projectId: string;
environmentId: string;
days: number;
tasks: string[];
}): Promise;
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,
});
diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts
index 8d9445ce7f..6d2befbb2c 100644
--- a/apps/webapp/app/services/runsRepository.server.ts
+++ b/apps/webapp/app/services/runsRepository.server.ts
@@ -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: {
diff --git a/apps/webapp/test/runsRepository.test.ts b/apps/webapp/test/runsRepository.test.ts
index 91c1f5de5e..7ee3b05b4f 100644
--- a/apps/webapp/test/runsRepository.test.ts
+++ b/apps/webapp/test/runsRepository.test.ts
@@ -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);
diff --git a/internal-packages/clickhouse/src/taskRuns.ts b/internal-packages/clickhouse/src/taskRuns.ts
index 396f990efa..b57ddbfc8b 100644
--- a/internal-packages/clickhouse/src/taskRuns.ts
+++ b/internal-packages/clickhouse/src/taskRuns.ts
@@ -113,6 +113,8 @@ export const TaskActivityQueryResult = z.object({
export type TaskActivityQueryResult = z.infer;
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;
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;
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