Skip to content

Commit 885d2d3

Browse files
authored
Tags listing now uses ClickHouse (#2576)
* WIP using ClickHouse for the tags filter list * WIP on tags listing * Webapp: exclude test files when typechecking * Tags filtering working with CH * Remove unused import * The AI filter should only look at the last past 30d of tags * Do the text query in ClickHouse * Deal with encoded characters better * More encoding fixes * Fix for wrong items being checked * Put applied tags back * Add the env.id to the dependencies array
1 parent a445af1 commit 885d2d3

13 files changed

+270
-80
lines changed

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ 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";
64-
import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters";
64+
import { appliedSummary, FilterMenuProvider, TimeFilter, timeFilters } from "./SharedFilters";
6565
import { AIFilterInput } from "./AIFilterInput";
6666
import {
6767
allTaskRunStatuses,
@@ -280,7 +280,7 @@ export function getRunFiltersFromSearchParams(
280280
bulkId: searchParams.get("bulkId") ?? undefined,
281281
tags:
282282
searchParams.getAll("tags").filter((v) => v.length > 0).length > 0
283-
? searchParams.getAll("tags").map((t) => decodeURIComponent(t))
283+
? searchParams.getAll("tags")
284284
: undefined,
285285
from: searchParams.get("from") ?? undefined,
286286
to: searchParams.get("to") ?? undefined,
@@ -810,8 +810,8 @@ function TagsDropdown({
810810
searchValue: string;
811811
onClose?: () => void;
812812
}) {
813-
const project = useProject();
814-
const { values, replace } = useSearchParams();
813+
const environment = useEnvironment();
814+
const { values, value, replace } = useSearchParams();
815815

816816
const handleChange = (values: string[]) => {
817817
clearSearchValue();
@@ -822,6 +822,12 @@ function TagsDropdown({
822822
});
823823
};
824824

825+
const { period, from, to } = timeFilters({
826+
period: value("period"),
827+
from: value("from"),
828+
to: value("to"),
829+
});
830+
825831
const tagValues = values("tags").filter((v) => v !== "");
826832
const selected = tagValues.length > 0 ? tagValues : undefined;
827833

@@ -830,25 +836,34 @@ function TagsDropdown({
830836
useEffect(() => {
831837
const searchParams = new URLSearchParams();
832838
if (searchValue) {
833-
searchParams.set("name", encodeURIComponent(searchValue));
839+
searchParams.set("name", searchValue);
834840
}
835-
fetcher.load(`/resources/projects/${project.slug}/runs/tags?${searchParams}`);
836-
}, [searchValue]);
841+
if (period) {
842+
searchParams.set("period", period);
843+
}
844+
if (from) {
845+
searchParams.set("from", from.getTime().toString());
846+
}
847+
if (to) {
848+
searchParams.set("to", to.getTime().toString());
849+
}
850+
fetcher.load(`/resources/environments/${environment.id}/runs/tags?${searchParams}`);
851+
}, [environment.id, searchValue, period, from?.getTime(), to?.getTime()]);
837852

838853
const filtered = useMemo(() => {
839854
let items: string[] = [];
840855
if (searchValue === "") {
841-
items = selected ?? [];
856+
items = [...(selected ?? [])];
842857
}
843858

844859
if (fetcher.data === undefined) {
845860
return matchSorter(items, searchValue);
846861
}
847862

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

850865
return matchSorter(Array.from(new Set(items)), searchValue);
851-
}, [searchValue, fetcher.data]);
866+
}, [searchValue, fetcher.data, selected]);
852867

853868
return (
854869
<SelectProvider value={selected ?? []} setValue={handleChange} virtualFocus={true}>
@@ -958,7 +973,7 @@ function QueuesDropdown({
958973
const searchParams = new URLSearchParams();
959974
searchParams.set("per_page", "25");
960975
if (searchValue) {
961-
searchParams.set("query", encodeURIComponent(s));
976+
searchParams.set("query", s);
962977
}
963978
fetcher.load(
964979
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${
@@ -1220,7 +1235,7 @@ function VersionsDropdown({
12201235
(s) => {
12211236
const searchParams = new URLSearchParams();
12221237
if (searchValue) {
1223-
searchParams.set("query", encodeURIComponent(s));
1238+
searchParams.set("query", s);
12241239
}
12251240
fetcher.load(
12261241
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ function TagsDropdown({
330330
useEffect(() => {
331331
const searchParams = new URLSearchParams();
332332
if (searchValue) {
333-
searchParams.set("name", encodeURIComponent(searchValue));
333+
searchParams.set("name", searchValue);
334334
}
335335
fetcher.load(
336336
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/waitpoints/tags?${searchParams}`

apps/webapp/app/hooks/useSearchParam.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,15 @@ export function useSearchParams() {
3333
const value = useCallback(
3434
(param: string) => {
3535
const search = new URLSearchParams(location.search);
36-
const val = search.get(param) ?? undefined;
37-
if (val === undefined) {
38-
return val;
39-
}
40-
41-
return decodeURIComponent(val);
36+
return search.get(param) ?? undefined;
4237
},
4338
[location]
4439
);
4540

4641
const values = useCallback(
4742
(param: string) => {
4843
const search = new URLSearchParams(location.search);
49-
const all = search.getAll(param);
50-
return all.map((v) => decodeURIComponent(v));
44+
return search.getAll(param);
5145
},
5246
[location]
5347
);

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

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
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";
5+
import { timeFilters } from "~/components/runs/v3/SharedFilters";
26

37
export type TagListOptions = {
4-
userId?: string;
8+
organizationId: string;
9+
environmentId: string;
510
projectId: string;
11+
period?: string;
12+
from?: Date;
13+
to?: Date;
614
//filters
715
name?: string;
816
//pagination
@@ -17,40 +25,39 @@ export type TagListItem = TagList["tags"][number];
1725

1826
export class RunTagListPresenter extends BasePresenter {
1927
public async call({
20-
userId,
28+
organizationId,
29+
environmentId,
2130
projectId,
2231
name,
32+
period,
33+
from,
34+
to,
2335
page = 1,
2436
pageSize = DEFAULT_PAGE_SIZE,
2537
}: TagListOptions) {
2638
const hasFilters = Boolean(name?.trim());
2739

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,
40+
const runsRepository = new RunsRepository({
41+
clickhouse: clickhouseClient,
42+
prisma: this._replica as PrismaClient,
43+
});
44+
45+
const tags = await runsRepository.listTags({
46+
organizationId,
47+
projectId,
48+
environmentId,
49+
query: name,
50+
period,
51+
from: from ? from.getTime() : undefined,
52+
to: to ? to.getTime() : undefined,
53+
offset: (page - 1) * pageSize,
54+
limit: pageSize + 1,
4355
});
4456

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

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,16 @@ export async function action({ request, params }: ActionFunctionArgs) {
6565
query: async (search) => {
6666
const tagPresenter = new RunTagListPresenter();
6767
const tags = await tagPresenter.call({
68+
organizationId: environment.organizationId,
6869
projectId: environment.projectId,
70+
environmentId: environment.id,
6971
name: search,
7072
page: 1,
7173
pageSize: 50,
74+
period: "30d",
7275
});
7376
return {
74-
tags: tags.tags.map((t) => t.name),
77+
tags: tags.tags,
7578
};
7679
},
7780
};

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

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

0 commit comments

Comments
 (0)