From 5e9f3e6a6f85a3b6e8057030142f5ba03cbafb82 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 26 Aug 2025 11:26:22 +0100 Subject: [PATCH 1/4] WIP using ClickHouse for the tags filter list --- .../app/components/runs/v3/RunFilters.tsx | 9 +-- .../v3/RunTagListPresenter.server.ts | 51 ++++++++-------- ...esources.environments.$envId.runs.tags.tsx | 61 +++++++++++++++++++ ...urces.projects.$projectParam.runs.tags.tsx | 32 ---------- .../clickhouseRunsRepository.server.ts | 31 ++++++++++ .../postgresRunsRepository.server.ts | 27 ++++++++ .../runsRepository/runsRepository.server.ts | 26 ++++++++ internal-packages/clickhouse/src/index.ts | 2 + internal-packages/clickhouse/src/taskRuns.ts | 11 ++++ 9 files changed, 190 insertions(+), 60 deletions(-) create mode 100644 apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx delete mode 100644 apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index f1153417cd..30b23661af 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -58,7 +58,7 @@ import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions"; -import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags"; +import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags"; import { Button } from "../../primitives/Buttons"; import { BulkActionTypeCombo } from "./BulkAction"; import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters"; @@ -71,6 +71,7 @@ import { TaskRunStatusCombo, } from "./TaskRunStatus"; import { TaskTriggerSourceIcon } from "./TaskTriggerSource"; +import { environment } from "effect/Differ"; export const RunStatus = z.enum(allTaskRunStatuses); @@ -810,7 +811,7 @@ function TagsDropdown({ searchValue: string; onClose?: () => void; }) { - const project = useProject(); + const environment = useEnvironment(); const { values, replace } = useSearchParams(); const handleChange = (values: string[]) => { @@ -832,7 +833,7 @@ function TagsDropdown({ if (searchValue) { searchParams.set("name", encodeURIComponent(searchValue)); } - fetcher.load(`/resources/projects/${project.slug}/runs/tags?${searchParams}`); + fetcher.load(`/resources/environments/${environment.id}/runs/tags?${searchParams}`); }, [searchValue]); const filtered = useMemo(() => { @@ -845,7 +846,7 @@ function TagsDropdown({ return matchSorter(items, searchValue); } - items.push(...fetcher.data.tags.map((t) => t.name)); + items.push(...fetcher.data.tags); return matchSorter(Array.from(new Set(items)), searchValue); }, [searchValue, fetcher.data]); diff --git a/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts index f159d3928e..9bdd8db5b0 100644 --- a/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts @@ -1,8 +1,13 @@ +import { RunsRepository } from "~/services/runsRepository/runsRepository.server"; import { BasePresenter } from "./basePresenter.server"; +import { clickhouseClient } from "~/services/clickhouseInstance.server"; +import { type PrismaClient } from "@trigger.dev/database"; export type TagListOptions = { - userId?: string; + organizationId: string; + environmentId: string; projectId: string; + createdAfter?: Date; //filters name?: string; //pagination @@ -17,40 +22,38 @@ export type TagListItem = TagList["tags"][number]; export class RunTagListPresenter extends BasePresenter { public async call({ - userId, + organizationId, + environmentId, projectId, name, + createdAfter, page = 1, pageSize = DEFAULT_PAGE_SIZE, }: TagListOptions) { const hasFilters = Boolean(name?.trim()); - const tags = await this._replica.taskRunTag.findMany({ - where: { - projectId, - name: name - ? { - startsWith: name, - mode: "insensitive", - } - : undefined, - }, - orderBy: { - id: "desc", - }, - take: pageSize + 1, - skip: (page - 1) * pageSize, + const runsRepository = new RunsRepository({ + clickhouse: clickhouseClient, + prisma: this._replica as PrismaClient, + }); + + // Passed in or past 30 days + const createdAt = createdAfter ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + + const tags = await runsRepository.listTags({ + organizationId, + projectId, + environmentId, + query: name, + createdAfter: createdAt, + offset: (page - 1) * pageSize, + limit: pageSize + 1, }); return { - tags: tags - .map((tag) => ({ - id: tag.friendlyId, - name: tag.name, - })) - .slice(0, pageSize), + tags: tags.tags, currentPage: page, - hasMore: tags.length > pageSize, + hasMore: tags.tags.length > pageSize, hasFilters, }; } diff --git a/apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx b/apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx new file mode 100644 index 0000000000..a4483fcd61 --- /dev/null +++ b/apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx @@ -0,0 +1,61 @@ +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { $replica } from "~/db.server"; +import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; +import { requireUserId } from "~/services/session.server"; + +const Params = z.object({ + envId: z.string(), +}); + +const SearchParams = z.object({ + name: z.string().optional(), + period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()), + from: z.coerce.number().optional(), + to: z.coerce.number().optional(), +}); + +export async function loader({ request, params }: LoaderFunctionArgs) { + const userId = await requireUserId(request); + const { envId } = Params.parse(params); + + const environment = await $replica.runtimeEnvironment.findFirst({ + select: { + id: true, + projectId: true, + organizationId: true, + }, + where: { id: envId, organization: { members: { some: { userId } } } }, + }); + + if (!environment) { + throw new Response("Not Found", { status: 404 }); + } + + const search = new URL(request.url).searchParams; + const name = search.get("name"); + const period = search.get("period"); + const from = search.get("from"); + const to = search.get("to"); + + const parsedSearchParams = SearchParams.safeParse({ + name: name ? decodeURIComponent(name) : undefined, + period: search.get("period") ?? undefined, + from: search.get("from") ?? undefined, + to: search.get("to") ?? undefined, + }); + + if (!parsedSearchParams.success) { + throw new Response("Invalid search params", { status: 400 }); + } + + const presenter = new RunTagListPresenter(); + const result = await presenter.call({ + environmentId: environment.id, + projectId: environment.projectId, + organizationId: environment.organizationId, + name: parsedSearchParams.data.name, + createdAfter: parsedSearchParams.data.createdAfter, + }); + return result; +} diff --git a/apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx b/apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx deleted file mode 100644 index 449142f53c..0000000000 --- a/apps/webapp/app/routes/resources.projects.$projectParam.runs.tags.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { z } from "zod"; -import { $replica } from "~/db.server"; -import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; -import { requireUserId } from "~/services/session.server"; - -const Params = z.object({ - projectParam: z.string(), -}); - -export async function loader({ request, params }: LoaderFunctionArgs) { - const userId = await requireUserId(request); - const { projectParam } = Params.parse(params); - - const project = await $replica.project.findFirst({ - where: { slug: projectParam, deletedAt: null, organization: { members: { some: { userId } } } }, - }); - - if (!project) { - throw new Response("Not Found", { status: 404 }); - } - - const search = new URL(request.url).searchParams; - const name = search.get("name"); - - const presenter = new RunTagListPresenter(); - const result = await presenter.call({ - projectId: project.id, - name: name ? decodeURIComponent(name) : undefined, - }); - return result; -} diff --git a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts index 56a42c751d..9609682d1e 100644 --- a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts @@ -6,6 +6,7 @@ import { type ListRunsOptions, type RunListInputOptions, type RunsRepositoryOptions, + TagListOptions, convertRunListInputOptionsToFilterRunsOptions, } from "./runsRepository.server"; @@ -162,6 +163,36 @@ export class ClickHouseRunsRepository implements IRunsRepository { return result[0].count; } + + async listTags(options: TagListOptions) { + const queryBuilder = this.options.clickhouse.taskRuns + .tagQueryBuilder() + .where("organization_id = {organizationId: String}", { + organizationId: options.organizationId, + }) + .where("project_id = {projectId: String}", { + projectId: options.projectId, + }) + .where("environment_id = {environmentId: String}", { + environmentId: options.environmentId, + }); + + const [queryError, result] = await queryBuilder.execute(); + + if (queryError) { + throw queryError; + } + + if (result.length === 0) { + throw new Error("No count rows returned"); + } + + //todo process them + + return { + tags: result[0].tags, + }; + } } function applyRunFiltersToQueryBuilder( diff --git a/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts index ec9b5be69b..554ccaf7c8 100644 --- a/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts @@ -8,6 +8,7 @@ import { type ListedRun, type RunListInputOptions, type RunsRepositoryOptions, + TagListOptions, convertRunListInputOptionsToFilterRunsOptions, } from "./runsRepository.server"; @@ -104,6 +105,32 @@ export class PostgresRunsRepository implements IRunsRepository { return Number(result[0].count); } + async listTags({ projectId, query, offset, limit }: TagListOptions) { + const tags = await this.options.prisma.taskRunTag.findMany({ + select: { + name: true, + }, + where: { + projectId, + name: query + ? { + startsWith: query, + mode: "insensitive", + } + : undefined, + }, + orderBy: { + id: "desc", + }, + take: limit + 1, + skip: offset, + }); + + return { + tags: tags.map((tag) => tag.name), + }; + } + #buildRunIdsQuery( filterOptions: FilterRunsOptions, page: { size: number; cursor?: string; direction?: "forward" | "backward" } diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts index 7b9bf2a368..8846609d45 100644 --- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts @@ -69,6 +69,11 @@ type Pagination = { }; }; +type OffsetPagination = { + offset: number; + limit: number; +}; + export type ListedRun = Prisma.TaskRunGetPayload<{ select: { id: true; @@ -104,6 +109,19 @@ export type ListedRun = Prisma.TaskRunGetPayload<{ export type ListRunsOptions = RunListInputOptions & Pagination; +export type TagListOptions = { + organizationId: string; + projectId: string; + environmentId: string; + createdAfter: Date; + /** Performs a case insensitive contains search on the tag name */ + query?: string; +} & OffsetPagination; + +export type TagList = { + tags: string[]; +}; + export interface IRunsRepository { name: string; listRunIds(options: ListRunsOptions): Promise; @@ -115,6 +133,7 @@ export interface IRunsRepository { }; }>; countRuns(options: RunListInputOptions): Promise; + listTags(options: TagListOptions): Promise; } export class RunsRepository implements IRunsRepository { @@ -291,6 +310,13 @@ export class RunsRepository implements IRunsRepository { } ); } + + async listTags(options: TagListOptions): Promise { + const repository = await this.#getRepository(); + return startActiveSpan("runsRepository.listTags", async () => { + return await repository.listTags(options); + }); + } } export function parseRunListInputOptions(data: any): RunListInputOptions { diff --git a/internal-packages/clickhouse/src/index.ts b/internal-packages/clickhouse/src/index.ts index 599492eb53..7d15b7b9bb 100644 --- a/internal-packages/clickhouse/src/index.ts +++ b/internal-packages/clickhouse/src/index.ts @@ -11,6 +11,7 @@ import { getAverageDurations, getTaskUsageByOrganization, getTaskRunsCountQueryBuilder, + getTaskRunTagsQueryBuilder, } from "./taskRuns.js"; import { Logger, type LogLevel } from "@trigger.dev/core/logger"; import type { Agent as HttpAgent } from "http"; @@ -147,6 +148,7 @@ export class ClickHouse { insertPayloads: insertRawTaskRunPayloads(this.writer), queryBuilder: getTaskRunsQueryBuilder(this.reader), countQueryBuilder: getTaskRunsCountQueryBuilder(this.reader), + tagQueryBuilder: getTaskRunTagsQueryBuilder(this.reader), getTaskActivity: getTaskActivityQueryBuilder(this.reader), getCurrentRunningStats: getCurrentRunningStats(this.reader), getAverageDurations: getAverageDurations(this.reader), diff --git a/internal-packages/clickhouse/src/taskRuns.ts b/internal-packages/clickhouse/src/taskRuns.ts index 2363c691fd..c20a41d822 100644 --- a/internal-packages/clickhouse/src/taskRuns.ts +++ b/internal-packages/clickhouse/src/taskRuns.ts @@ -115,6 +115,17 @@ export function getTaskRunsCountQueryBuilder(ch: ClickhouseReader, settings?: Cl }); } +export function getTaskRunTagsQueryBuilder(ch: ClickhouseReader, settings?: ClickHouseSettings) { + return ch.queryBuilder({ + name: "getTaskRunTags", + baseQuery: "SELECT DISTINCT tags FROM trigger_dev.task_runs_v2", + schema: z.object({ + tags: z.array(z.string()), + }), + settings, + }); +} + export const TaskActivityQueryResult = z.object({ task_identifier: z.string(), status: z.string(), From dff5e7dd4f8e1b627244a66317f0b9b41caae465 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 23 Sep 2025 14:14:25 +0100 Subject: [PATCH 2/4] WIP on tags listing --- .../v3/RunTagListPresenter.server.ts | 16 +++++++++----- ...esources.environments.$envId.runs.tags.tsx | 14 ++++++++---- ...jectParam.env.$envParam.runs.ai-filter.tsx | 5 ++++- .../clickhouseRunsRepository.server.ts | 22 ++++++++++++++++--- .../postgresRunsRepository.server.ts | 2 +- .../runsRepository/runsRepository.server.ts | 21 ++++++++++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts index 9bdd8db5b0..e9de368ece 100644 --- a/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunTagListPresenter.server.ts @@ -2,12 +2,15 @@ import { RunsRepository } from "~/services/runsRepository/runsRepository.server" import { BasePresenter } from "./basePresenter.server"; import { clickhouseClient } from "~/services/clickhouseInstance.server"; import { type PrismaClient } from "@trigger.dev/database"; +import { timeFilters } from "~/components/runs/v3/SharedFilters"; export type TagListOptions = { organizationId: string; environmentId: string; projectId: string; - createdAfter?: Date; + period?: string; + from?: Date; + to?: Date; //filters name?: string; //pagination @@ -26,7 +29,9 @@ export class RunTagListPresenter extends BasePresenter { environmentId, projectId, name, - createdAfter, + period, + from, + to, page = 1, pageSize = DEFAULT_PAGE_SIZE, }: TagListOptions) { @@ -37,15 +42,14 @@ export class RunTagListPresenter extends BasePresenter { prisma: this._replica as PrismaClient, }); - // Passed in or past 30 days - const createdAt = createdAfter ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); - const tags = await runsRepository.listTags({ organizationId, projectId, environmentId, query: name, - createdAfter: createdAt, + period, + from: from ? from.getTime() : undefined, + to: to ? to.getTime() : undefined, offset: (page - 1) * pageSize, limit: pageSize + 1, }); diff --git a/apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx b/apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx index a4483fcd61..bff7f769ef 100644 --- a/apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx +++ b/apps/webapp/app/routes/resources.environments.$envId.runs.tags.tsx @@ -1,5 +1,6 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { z } from "zod"; +import { timeFilters } from "~/components/runs/v3/SharedFilters"; import { $replica } from "~/db.server"; import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; import { requireUserId } from "~/services/session.server"; @@ -34,9 +35,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const search = new URL(request.url).searchParams; const name = search.get("name"); - const period = search.get("period"); - const from = search.get("from"); - const to = search.get("to"); const parsedSearchParams = SearchParams.safeParse({ name: name ? decodeURIComponent(name) : undefined, @@ -49,13 +47,21 @@ export async function loader({ request, params }: LoaderFunctionArgs) { throw new Response("Invalid search params", { status: 400 }); } + const { period, from, to } = timeFilters({ + period: parsedSearchParams.data.period, + from: parsedSearchParams.data.from, + to: parsedSearchParams.data.to, + }); + const presenter = new RunTagListPresenter(); const result = await presenter.call({ environmentId: environment.id, projectId: environment.projectId, organizationId: environment.organizationId, name: parsedSearchParams.data.name, - createdAfter: parsedSearchParams.data.createdAfter, + period, + from, + to, }); return result; } diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index 9249d7afe9..2eddce4711 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -65,13 +65,16 @@ export async function action({ request, params }: ActionFunctionArgs) { query: async (search) => { const tagPresenter = new RunTagListPresenter(); const tags = await tagPresenter.call({ + organizationId: environment.organizationId, projectId: environment.projectId, + environmentId: environment.id, name: search, page: 1, pageSize: 50, + period: "1y", }); return { - tags: tags.tags.map((t) => t.name), + tags: tags.tags, }; }, }; diff --git a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts index 9609682d1e..46a7dcdde8 100644 --- a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts @@ -6,9 +6,10 @@ import { type ListRunsOptions, type RunListInputOptions, type RunsRepositoryOptions, - TagListOptions, + type TagListOptions, convertRunListInputOptionsToFilterRunsOptions, } from "./runsRepository.server"; +import parseDuration from "parse-duration"; export class ClickHouseRunsRepository implements IRunsRepository { constructor(private readonly options: RunsRepositoryOptions) {} @@ -177,6 +178,23 @@ export class ClickHouseRunsRepository implements IRunsRepository { environmentId: options.environmentId, }); + const periodMs = options.period ? parseDuration(options.period) ?? undefined : undefined; + if (periodMs) { + queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", { + period: new Date(Date.now() - periodMs).getTime(), + }); + } + + if (options.from) { + queryBuilder.where("created_at >= fromUnixTimestamp64Milli({from: Int64})", { + from: options.from, + }); + } + + if (options.to) { + queryBuilder.where("created_at <= fromUnixTimestamp64Milli({to: Int64})", { to: options.to }); + } + const [queryError, result] = await queryBuilder.execute(); if (queryError) { @@ -187,8 +205,6 @@ export class ClickHouseRunsRepository implements IRunsRepository { throw new Error("No count rows returned"); } - //todo process them - return { tags: result[0].tags, }; diff --git a/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts index 554ccaf7c8..93edbd9349 100644 --- a/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/postgresRunsRepository.server.ts @@ -8,7 +8,7 @@ import { type ListedRun, type RunListInputOptions, type RunsRepositoryOptions, - TagListOptions, + type TagListOptions, convertRunListInputOptionsToFilterRunsOptions, } from "./runsRepository.server"; diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts index 8846609d45..7bf81a4aa5 100644 --- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts @@ -113,7 +113,9 @@ export type TagListOptions = { organizationId: string; projectId: string; environmentId: string; - createdAfter: Date; + period?: string; + from?: number; + to?: number; /** Performs a case insensitive contains search on the tag name */ query?: string; } & OffsetPagination; @@ -313,9 +315,20 @@ export class RunsRepository implements IRunsRepository { async listTags(options: TagListOptions): Promise { const repository = await this.#getRepository(); - return startActiveSpan("runsRepository.listTags", async () => { - return await repository.listTags(options); - }); + return startActiveSpan( + "runsRepository.listTags", + async () => { + return await repository.listTags(options); + }, + { + attributes: { + "repository.name": repository.name, + organizationId: options.organizationId, + projectId: options.projectId, + environmentId: options.environmentId, + }, + } + ); } } From e63cbd88aa1f836b1bfdebfc1cdebc5707a93a73 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 30 Sep 2025 17:26:05 -0700 Subject: [PATCH 3/4] Webapp: exclude test files when typechecking --- apps/webapp/tsconfig.check.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/webapp/tsconfig.check.json b/apps/webapp/tsconfig.check.json index 091b4ddb36..cb73affedf 100644 --- a/apps/webapp/tsconfig.check.json +++ b/apps/webapp/tsconfig.check.json @@ -9,5 +9,6 @@ "@/*": ["./*"] }, "customConditions": [] - } + }, + "exclude": ["**/*.test.ts", "**/*.test.tsx"] } From 64961712622db46ff2405c49b51af7b3ffe6ca68 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 30 Sep 2025 18:26:12 -0700 Subject: [PATCH 4/4] Tags filtering working with CH --- .../app/components/runs/v3/RunFilters.tsx | 21 ++++++++++++++++--- .../clickhouseRunsRepository.server.ts | 6 ++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 30b23661af..cc0136a9b7 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -61,7 +61,7 @@ import { type loader as versionsLoader } from "~/routes/resources.orgs.$organiza import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags"; import { Button } from "../../primitives/Buttons"; import { BulkActionTypeCombo } from "./BulkAction"; -import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters"; +import { appliedSummary, FilterMenuProvider, TimeFilter, timeFilters } from "./SharedFilters"; import { AIFilterInput } from "./AIFilterInput"; import { allTaskRunStatuses, @@ -812,7 +812,7 @@ function TagsDropdown({ onClose?: () => void; }) { const environment = useEnvironment(); - const { values, replace } = useSearchParams(); + const { values, value, replace } = useSearchParams(); const handleChange = (values: string[]) => { clearSearchValue(); @@ -823,6 +823,12 @@ function TagsDropdown({ }); }; + const { period, from, to } = timeFilters({ + period: value("period"), + from: value("from"), + to: value("to"), + }); + const tagValues = values("tags").filter((v) => v !== ""); const selected = tagValues.length > 0 ? tagValues : undefined; @@ -833,8 +839,17 @@ function TagsDropdown({ if (searchValue) { searchParams.set("name", encodeURIComponent(searchValue)); } + if (period) { + searchParams.set("period", period); + } + if (from) { + searchParams.set("from", from.getTime().toString()); + } + if (to) { + searchParams.set("to", to.getTime().toString()); + } fetcher.load(`/resources/environments/${environment.id}/runs/tags?${searchParams}`); - }, [searchValue]); + }, [searchValue, period, from?.getTime(), to?.getTime()]); const filtered = useMemo(() => { let items: string[] = []; diff --git a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts index 46a7dcdde8..dbf6a584c3 100644 --- a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts @@ -202,11 +202,13 @@ export class ClickHouseRunsRepository implements IRunsRepository { } if (result.length === 0) { - throw new Error("No count rows returned"); + return { + tags: [], + }; } return { - tags: result[0].tags, + tags: result.flatMap((row) => row.tags), }; } }