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