Skip to content

Commit 5e9f3e6

Browse files
committed
WIP using ClickHouse for the tags filter list
1 parent 0597691 commit 5e9f3e6

File tree

9 files changed

+190
-60
lines changed

9 files changed

+190
-60
lines changed

apps/webapp/app/components/runs/v3/RunFilters.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import { useProject } from "~/hooks/useProject";
5858
import { useSearchParams } from "~/hooks/useSearchParam";
5959
import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues";
6060
import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions";
61-
import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags";
61+
import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags";
6262
import { Button } from "../../primitives/Buttons";
6363
import { BulkActionTypeCombo } from "./BulkAction";
6464
import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters";
@@ -71,6 +71,7 @@ import {
7171
TaskRunStatusCombo,
7272
} from "./TaskRunStatus";
7373
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
74+
import { environment } from "effect/Differ";
7475

7576
export const RunStatus = z.enum(allTaskRunStatuses);
7677

@@ -810,7 +811,7 @@ function TagsDropdown({
810811
searchValue: string;
811812
onClose?: () => void;
812813
}) {
813-
const project = useProject();
814+
const environment = useEnvironment();
814815
const { values, replace } = useSearchParams();
815816

816817
const handleChange = (values: string[]) => {
@@ -832,7 +833,7 @@ function TagsDropdown({
832833
if (searchValue) {
833834
searchParams.set("name", encodeURIComponent(searchValue));
834835
}
835-
fetcher.load(`/resources/projects/${project.slug}/runs/tags?${searchParams}`);
836+
fetcher.load(`/resources/environments/${environment.id}/runs/tags?${searchParams}`);
836837
}, [searchValue]);
837838

838839
const filtered = useMemo(() => {
@@ -845,7 +846,7 @@ function TagsDropdown({
845846
return matchSorter(items, searchValue);
846847
}
847848

848-
items.push(...fetcher.data.tags.map((t) => t.name));
849+
items.push(...fetcher.data.tags);
849850

850851
return matchSorter(Array.from(new Set(items)), searchValue);
851852
}, [searchValue, fetcher.data]);

apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import { RunsRepository } from "~/services/runsRepository/runsRepository.server";
12
import { BasePresenter } from "./basePresenter.server";
3+
import { clickhouseClient } from "~/services/clickhouseInstance.server";
4+
import { type PrismaClient } from "@trigger.dev/database";
25

36
export type TagListOptions = {
4-
userId?: string;
7+
organizationId: string;
8+
environmentId: string;
59
projectId: string;
10+
createdAfter?: Date;
611
//filters
712
name?: string;
813
//pagination
@@ -17,40 +22,38 @@ export type TagListItem = TagList["tags"][number];
1722

1823
export class RunTagListPresenter extends BasePresenter {
1924
public async call({
20-
userId,
25+
organizationId,
26+
environmentId,
2127
projectId,
2228
name,
29+
createdAfter,
2330
page = 1,
2431
pageSize = DEFAULT_PAGE_SIZE,
2532
}: TagListOptions) {
2633
const hasFilters = Boolean(name?.trim());
2734

28-
const tags = await this._replica.taskRunTag.findMany({
29-
where: {
30-
projectId,
31-
name: name
32-
? {
33-
startsWith: name,
34-
mode: "insensitive",
35-
}
36-
: undefined,
37-
},
38-
orderBy: {
39-
id: "desc",
40-
},
41-
take: pageSize + 1,
42-
skip: (page - 1) * pageSize,
35+
const runsRepository = new RunsRepository({
36+
clickhouse: clickhouseClient,
37+
prisma: this._replica as PrismaClient,
38+
});
39+
40+
// Passed in or past 30 days
41+
const createdAt = createdAfter ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
42+
43+
const tags = await runsRepository.listTags({
44+
organizationId,
45+
projectId,
46+
environmentId,
47+
query: name,
48+
createdAfter: createdAt,
49+
offset: (page - 1) * pageSize,
50+
limit: pageSize + 1,
4351
});
4452

4553
return {
46-
tags: tags
47-
.map((tag) => ({
48-
id: tag.friendlyId,
49-
name: tag.name,
50-
}))
51-
.slice(0, pageSize),
54+
tags: tags.tags,
5255
currentPage: page,
53-
hasMore: tags.length > pageSize,
56+
hasMore: tags.tags.length > pageSize,
5457
hasFilters,
5558
};
5659
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { $replica } from "~/db.server";
4+
import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server";
5+
import { requireUserId } from "~/services/session.server";
6+
7+
const Params = z.object({
8+
envId: z.string(),
9+
});
10+
11+
const SearchParams = z.object({
12+
name: z.string().optional(),
13+
period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()),
14+
from: z.coerce.number().optional(),
15+
to: z.coerce.number().optional(),
16+
});
17+
18+
export async function loader({ request, params }: LoaderFunctionArgs) {
19+
const userId = await requireUserId(request);
20+
const { envId } = Params.parse(params);
21+
22+
const environment = await $replica.runtimeEnvironment.findFirst({
23+
select: {
24+
id: true,
25+
projectId: true,
26+
organizationId: true,
27+
},
28+
where: { id: envId, organization: { members: { some: { userId } } } },
29+
});
30+
31+
if (!environment) {
32+
throw new Response("Not Found", { status: 404 });
33+
}
34+
35+
const search = new URL(request.url).searchParams;
36+
const name = search.get("name");
37+
const period = search.get("period");
38+
const from = search.get("from");
39+
const to = search.get("to");
40+
41+
const parsedSearchParams = SearchParams.safeParse({
42+
name: name ? decodeURIComponent(name) : undefined,
43+
period: search.get("period") ?? undefined,
44+
from: search.get("from") ?? undefined,
45+
to: search.get("to") ?? undefined,
46+
});
47+
48+
if (!parsedSearchParams.success) {
49+
throw new Response("Invalid search params", { status: 400 });
50+
}
51+
52+
const presenter = new RunTagListPresenter();
53+
const result = await presenter.call({
54+
environmentId: environment.id,
55+
projectId: environment.projectId,
56+
organizationId: environment.organizationId,
57+
name: parsedSearchParams.data.name,
58+
createdAfter: parsedSearchParams.data.createdAfter,
59+
});
60+
return result;
61+
}

apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx

Lines changed: 0 additions & 32 deletions
This file was deleted.

apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type ListRunsOptions,
77
type RunListInputOptions,
88
type RunsRepositoryOptions,
9+
TagListOptions,
910
convertRunListInputOptionsToFilterRunsOptions,
1011
} from "./runsRepository.server";
1112

@@ -162,6 +163,36 @@ export class ClickHouseRunsRepository implements IRunsRepository {
162163

163164
return result[0].count;
164165
}
166+
167+
async listTags(options: TagListOptions) {
168+
const queryBuilder = this.options.clickhouse.taskRuns
169+
.tagQueryBuilder()
170+
.where("organization_id = {organizationId: String}", {
171+
organizationId: options.organizationId,
172+
})
173+
.where("project_id = {projectId: String}", {
174+
projectId: options.projectId,
175+
})
176+
.where("environment_id = {environmentId: String}", {
177+
environmentId: options.environmentId,
178+
});
179+
180+
const [queryError, result] = await queryBuilder.execute();
181+
182+
if (queryError) {
183+
throw queryError;
184+
}
185+
186+
if (result.length === 0) {
187+
throw new Error("No count rows returned");
188+
}
189+
190+
//todo process them
191+
192+
return {
193+
tags: result[0].tags,
194+
};
195+
}
165196
}
166197

167198
function applyRunFiltersToQueryBuilder<T>(

apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type ListedRun,
99
type RunListInputOptions,
1010
type RunsRepositoryOptions,
11+
TagListOptions,
1112
convertRunListInputOptionsToFilterRunsOptions,
1213
} from "./runsRepository.server";
1314

@@ -104,6 +105,32 @@ export class PostgresRunsRepository implements IRunsRepository {
104105
return Number(result[0].count);
105106
}
106107

108+
async listTags({ projectId, query, offset, limit }: TagListOptions) {
109+
const tags = await this.options.prisma.taskRunTag.findMany({
110+
select: {
111+
name: true,
112+
},
113+
where: {
114+
projectId,
115+
name: query
116+
? {
117+
startsWith: query,
118+
mode: "insensitive",
119+
}
120+
: undefined,
121+
},
122+
orderBy: {
123+
id: "desc",
124+
},
125+
take: limit + 1,
126+
skip: offset,
127+
});
128+
129+
return {
130+
tags: tags.map((tag) => tag.name),
131+
};
132+
}
133+
107134
#buildRunIdsQuery(
108135
filterOptions: FilterRunsOptions,
109136
page: { size: number; cursor?: string; direction?: "forward" | "backward" }

apps/webapp/app/services/runsRepository/runsRepository.server.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ type Pagination = {
6969
};
7070
};
7171

72+
type OffsetPagination = {
73+
offset: number;
74+
limit: number;
75+
};
76+
7277
export type ListedRun = Prisma.TaskRunGetPayload<{
7378
select: {
7479
id: true;
@@ -104,6 +109,19 @@ export type ListedRun = Prisma.TaskRunGetPayload<{
104109

105110
export type ListRunsOptions = RunListInputOptions & Pagination;
106111

112+
export type TagListOptions = {
113+
organizationId: string;
114+
projectId: string;
115+
environmentId: string;
116+
createdAfter: Date;
117+
/** Performs a case insensitive contains search on the tag name */
118+
query?: string;
119+
} & OffsetPagination;
120+
121+
export type TagList = {
122+
tags: string[];
123+
};
124+
107125
export interface IRunsRepository {
108126
name: string;
109127
listRunIds(options: ListRunsOptions): Promise<string[]>;
@@ -115,6 +133,7 @@ export interface IRunsRepository {
115133
};
116134
}>;
117135
countRuns(options: RunListInputOptions): Promise<number>;
136+
listTags(options: TagListOptions): Promise<TagList>;
118137
}
119138

120139
export class RunsRepository implements IRunsRepository {
@@ -291,6 +310,13 @@ export class RunsRepository implements IRunsRepository {
291310
}
292311
);
293312
}
313+
314+
async listTags(options: TagListOptions): Promise<TagList> {
315+
const repository = await this.#getRepository();
316+
return startActiveSpan("runsRepository.listTags", async () => {
317+
return await repository.listTags(options);
318+
});
319+
}
294320
}
295321

296322
export function parseRunListInputOptions(data: any): RunListInputOptions {

internal-packages/clickhouse/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getAverageDurations,
1212
getTaskUsageByOrganization,
1313
getTaskRunsCountQueryBuilder,
14+
getTaskRunTagsQueryBuilder,
1415
} from "./taskRuns.js";
1516
import { Logger, type LogLevel } from "@trigger.dev/core/logger";
1617
import type { Agent as HttpAgent } from "http";
@@ -147,6 +148,7 @@ export class ClickHouse {
147148
insertPayloads: insertRawTaskRunPayloads(this.writer),
148149
queryBuilder: getTaskRunsQueryBuilder(this.reader),
149150
countQueryBuilder: getTaskRunsCountQueryBuilder(this.reader),
151+
tagQueryBuilder: getTaskRunTagsQueryBuilder(this.reader),
150152
getTaskActivity: getTaskActivityQueryBuilder(this.reader),
151153
getCurrentRunningStats: getCurrentRunningStats(this.reader),
152154
getAverageDurations: getAverageDurations(this.reader),

internal-packages/clickhouse/src/taskRuns.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ export function getTaskRunsCountQueryBuilder(ch: ClickhouseReader, settings?: Cl
115115
});
116116
}
117117

118+
export function getTaskRunTagsQueryBuilder(ch: ClickhouseReader, settings?: ClickHouseSettings) {
119+
return ch.queryBuilder({
120+
name: "getTaskRunTags",
121+
baseQuery: "SELECT DISTINCT tags FROM trigger_dev.task_runs_v2",
122+
schema: z.object({
123+
tags: z.array(z.string()),
124+
}),
125+
settings,
126+
});
127+
}
128+
118129
export const TaskActivityQueryResult = z.object({
119130
task_identifier: z.string(),
120131
status: z.string(),

0 commit comments

Comments
 (0)