Initial work on the new bulk actions
This commit is contained in:
@@ -91,6 +91,24 @@ export const TaskRunListSearchFilters = z.object({
|
||||
|
||||
export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
|
||||
|
||||
export function getRunFiltersFromSearchParams(searchParams: URLSearchParams) {
|
||||
return {
|
||||
cursor: searchParams.get("cursor") ?? undefined,
|
||||
direction: searchParams.get("direction") ?? undefined,
|
||||
statuses: searchParams.getAll("statuses"),
|
||||
tasks: searchParams.getAll("tasks"),
|
||||
period: searchParams.get("period") ?? undefined,
|
||||
bulkId: searchParams.get("bulkId") ?? undefined,
|
||||
tags: searchParams.getAll("tags").map((t) => decodeURIComponent(t)),
|
||||
from: searchParams.get("from") ?? undefined,
|
||||
to: searchParams.get("to") ?? undefined,
|
||||
rootOnly: searchParams.has("rootOnly") ? searchParams.get("rootOnly") === "true" : undefined,
|
||||
runId: searchParams.get("runId") ?? undefined,
|
||||
batchId: searchParams.get("batchId") ?? undefined,
|
||||
scheduleId: searchParams.get("scheduleId") ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
type RunFiltersProps = {
|
||||
possibleTasks: { slug: string; triggerSource: TaskTriggerSource }[];
|
||||
bulkActions: {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
|
||||
import {
|
||||
getRunFiltersFromSearchParams,
|
||||
TaskRunListSearchFilters,
|
||||
} from "~/components/runs/v3/RunFilters";
|
||||
import { getRootOnlyFilterPreference } from "~/services/preferences/uiPreferences.server";
|
||||
|
||||
export async function getRunFiltersFromRequest(request: Request) {
|
||||
@@ -10,21 +13,7 @@ export async function getRunFiltersFromRequest(request: Request) {
|
||||
rootOnlyValue = await getRootOnlyFilterPreference(request);
|
||||
}
|
||||
|
||||
const s = {
|
||||
cursor: url.searchParams.get("cursor") ?? undefined,
|
||||
direction: url.searchParams.get("direction") ?? undefined,
|
||||
statuses: url.searchParams.getAll("statuses"),
|
||||
tasks: url.searchParams.getAll("tasks"),
|
||||
period: url.searchParams.get("period") ?? undefined,
|
||||
bulkId: url.searchParams.get("bulkId") ?? undefined,
|
||||
tags: url.searchParams.getAll("tags").map((t) => decodeURIComponent(t)),
|
||||
from: url.searchParams.get("from") ?? undefined,
|
||||
to: url.searchParams.get("to") ?? undefined,
|
||||
rootOnly: rootOnlyValue,
|
||||
runId: url.searchParams.get("runId") ?? undefined,
|
||||
batchId: url.searchParams.get("batchId") ?? undefined,
|
||||
scheduleId: url.searchParams.get("scheduleId") ?? undefined,
|
||||
};
|
||||
const s = getRunFiltersFromSearchParams(url.searchParams);
|
||||
|
||||
const {
|
||||
tasks,
|
||||
|
||||
+90
-118
@@ -57,6 +57,15 @@ import {
|
||||
v3TestPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { ListPagination } from "../../components/ListPagination";
|
||||
import { getRunFiltersFromRequest } from "~/presenters/RunFilters.server";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/primitives/Resizable";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
import { CreateBulkActionInspector } from "../resources.orgs.$organizationId.projects.$projectId.environments.$environmentId.runs.bulkaction";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -70,15 +79,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
let rootOnlyValue = false;
|
||||
if (url.searchParams.has("rootOnly")) {
|
||||
rootOnlyValue = url.searchParams.get("rootOnly") === "true";
|
||||
} else {
|
||||
rootOnlyValue = await getRootOnlyFilterPreference(request);
|
||||
}
|
||||
|
||||
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
||||
if (!project) {
|
||||
throw new Error("Project not found");
|
||||
@@ -89,71 +89,27 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
const s = {
|
||||
cursor: url.searchParams.get("cursor") ?? undefined,
|
||||
direction: url.searchParams.get("direction") ?? undefined,
|
||||
statuses: url.searchParams.getAll("statuses"),
|
||||
environments: [environment.id],
|
||||
tasks: url.searchParams.getAll("tasks"),
|
||||
period: url.searchParams.get("period") ?? undefined,
|
||||
bulkId: url.searchParams.get("bulkId") ?? undefined,
|
||||
tags: url.searchParams.getAll("tags").map((t) => decodeURIComponent(t)),
|
||||
from: url.searchParams.get("from") ?? undefined,
|
||||
to: url.searchParams.get("to") ?? undefined,
|
||||
rootOnly: rootOnlyValue,
|
||||
runId: url.searchParams.get("runId") ?? undefined,
|
||||
batchId: url.searchParams.get("batchId") ?? undefined,
|
||||
scheduleId: url.searchParams.get("scheduleId") ?? undefined,
|
||||
};
|
||||
const {
|
||||
tasks,
|
||||
versions,
|
||||
statuses,
|
||||
environments,
|
||||
tags,
|
||||
period,
|
||||
bulkId,
|
||||
from,
|
||||
to,
|
||||
cursor,
|
||||
direction,
|
||||
rootOnly,
|
||||
runId,
|
||||
batchId,
|
||||
scheduleId,
|
||||
} = TaskRunListSearchFilters.parse(s);
|
||||
|
||||
if (!clickhouseClient) {
|
||||
throw new Error("Clickhouse is not supported yet");
|
||||
}
|
||||
|
||||
const filters = await getRunFiltersFromRequest(request);
|
||||
|
||||
const presenter = new NextRunListPresenter($replica, clickhouseClient);
|
||||
const list = presenter.call(project.organizationId, environment.id, {
|
||||
userId,
|
||||
projectId: project.id,
|
||||
tasks,
|
||||
versions,
|
||||
statuses,
|
||||
tags,
|
||||
period,
|
||||
bulkId,
|
||||
from,
|
||||
to,
|
||||
batchId,
|
||||
runIds: runId ? [runId] : undefined,
|
||||
scheduleId,
|
||||
rootOnly,
|
||||
direction: direction,
|
||||
cursor: cursor,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const session = await setRootOnlyFilterPreference(rootOnlyValue, request);
|
||||
const session = await setRootOnlyFilterPreference(filters.rootOnly, request);
|
||||
const cookieValue = await uiPreferencesStorage.commitSession(session);
|
||||
|
||||
return typeddefer(
|
||||
{
|
||||
data: list,
|
||||
rootOnlyDefault: rootOnlyValue,
|
||||
rootOnlyDefault: filters.rootOnly,
|
||||
filters,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
@@ -164,12 +120,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { data, rootOnlyDefault } = useTypedLoaderData<typeof loader>();
|
||||
const { data, rootOnlyDefault, filters } = useTypedLoaderData<typeof loader>();
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
const { isConnected } = useDevPresence();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const isShowingBulkActionInspector = searchParams.has("bulkInspector");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -194,65 +154,77 @@ export default function Page() {
|
||||
maxSelectedItemCount={BULK_ACTION_RUN_LIMIT}
|
||||
>
|
||||
{({ selectedItems }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full overflow-hidden",
|
||||
selectedItems.size === 0 ? "grid-rows-1" : "grid-rows-[1fr_auto]"
|
||||
)}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<div className="mx-auto flex items-center gap-2">
|
||||
<Spinner />
|
||||
<Paragraph variant="small">Loading runs</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TypedAwait resolve={data}>
|
||||
{(list) => (
|
||||
<>
|
||||
{list.runs.length === 0 && !list.hasAnyRuns ? (
|
||||
list.possibleTasks.length === 0 ? (
|
||||
<CreateFirstTaskInstructions />
|
||||
) : (
|
||||
<RunTaskInstructions />
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full grid-rows-[auto_1fr] overflow-hidden"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-x-2 p-2">
|
||||
<RunsFilters
|
||||
possibleTasks={list.possibleTasks}
|
||||
bulkActions={list.bulkActions}
|
||||
hasFilters={list.hasFilters}
|
||||
rootOnlyDefault={rootOnlyDefault}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TaskRunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={list.hasFilters}
|
||||
filters={list.filters}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
allowSelection
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
|
||||
<ResizablePanel id="runs-main" min={"100px"}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full overflow-hidden",
|
||||
selectedItems.size === 0 ? "grid-rows-1" : "grid-rows-[1fr_auto]"
|
||||
)}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
<BulkActionBar />
|
||||
</div>
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<div className="mx-auto flex items-center gap-2">
|
||||
<Spinner />
|
||||
<Paragraph variant="small">Loading runs</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TypedAwait resolve={data}>
|
||||
{(list) => (
|
||||
<>
|
||||
{list.runs.length === 0 && !list.hasAnyRuns ? (
|
||||
list.possibleTasks.length === 0 ? (
|
||||
<CreateFirstTaskInstructions />
|
||||
) : (
|
||||
<RunTaskInstructions />
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full grid-rows-[auto_1fr] overflow-hidden"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-x-2 p-2">
|
||||
<RunsFilters
|
||||
possibleTasks={list.possibleTasks}
|
||||
bulkActions={list.bulkActions}
|
||||
hasFilters={list.hasFilters}
|
||||
rootOnlyDefault={rootOnlyDefault}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TaskRunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={list.hasFilters}
|
||||
filters={list.filters}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
allowSelection
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
<BulkActionBar />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
{isShowingBulkActionInspector && (
|
||||
<>
|
||||
<ResizableHandle id="runs-handle" />
|
||||
<ResizablePanel id="bulk-action-inspector" min="100px" default="450px">
|
||||
<CreateBulkActionInspector filters={filters} />
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
)}
|
||||
</SelectedItemsProvider>
|
||||
</PageBody>
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
||||
import { XCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Form, useActionData, useFetcher } from "@remix-run/react";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/router";
|
||||
import { useEffect } from "react";
|
||||
import { typedjson, useTypedFetcher } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
|
||||
import { $replica, type PrismaClient } from "~/db.server";
|
||||
import { useEnvironment } from "~/hooks/useEnvironment";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { getRunFiltersFromRequest } from "~/presenters/RunFilters.server";
|
||||
import { clickhouseClient } from "~/services/clickhouseInstance.server";
|
||||
import { RunsRepository } from "~/services/runsRepository.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { v3RunsPath } from "~/utils/pathBuilder";
|
||||
|
||||
const Params = z.object({
|
||||
organizationId: z.string(),
|
||||
projectId: z.string(),
|
||||
environmentId: z.string(),
|
||||
});
|
||||
|
||||
const searchParams = z.object({
|
||||
mode: z.union([z.literal("selected"), z.literal("filter")]).default("filter"),
|
||||
action: z.union([z.literal("cancel"), z.literal("replay")]).default("cancel"),
|
||||
});
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const { organizationId, projectId, environmentId } = Params.parse(params);
|
||||
const filters = await getRunFiltersFromRequest(request);
|
||||
const { mode, action } = searchParams.parse(
|
||||
Object.fromEntries(new URL(request.url).searchParams)
|
||||
);
|
||||
|
||||
//todo do a ClickHouse Query with the filters
|
||||
if (!clickhouseClient) {
|
||||
throw new Error("Clickhouse client not found");
|
||||
}
|
||||
|
||||
const runsRepository = new RunsRepository({
|
||||
clickhouse: clickhouseClient,
|
||||
prisma: $replica as PrismaClient,
|
||||
});
|
||||
|
||||
const count = await runsRepository.countRuns({
|
||||
organizationId,
|
||||
projectId,
|
||||
environmentId,
|
||||
// ...filters,
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
filters,
|
||||
mode,
|
||||
action,
|
||||
count,
|
||||
});
|
||||
}
|
||||
|
||||
export async function action({ params, request }: ActionFunctionArgs) {
|
||||
const { organizationId, projectId, environmentId } = Params.parse(params);
|
||||
const filters = await getRunFiltersFromRequest(request);
|
||||
|
||||
return redirectWithSuccessMessage("/", request, "SORTED");
|
||||
}
|
||||
|
||||
export function CreateBulkActionInspector({ filters }: { filters: TaskRunListSearchFilters }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
const fetcher = useTypedFetcher<typeof loader>();
|
||||
const lastSubmission = useActionData<typeof action>();
|
||||
const { value } = useSearchParams();
|
||||
const location = useOptimisticLocation();
|
||||
|
||||
useEffect(() => {
|
||||
fetcher.load(
|
||||
`/resources/orgs/${organization.id}/projects/${project.id}/environments/${environment.id}/runs/bulkaction${location.search}`
|
||||
);
|
||||
}, [organization.id, project.id, environment.id, location.search]);
|
||||
|
||||
const mode = value("mode");
|
||||
const action = value("action");
|
||||
|
||||
const data = fetcher.data != null ? fetcher.data : undefined;
|
||||
|
||||
return (
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
<Header2 className="whitespace-nowrap">Create a bulk action</Header2>
|
||||
<LinkButton
|
||||
to={`${v3RunsPath(organization, project, environment)}${location.search}`}
|
||||
variant="minimal/small"
|
||||
TrailingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
shortcutPosition="before-trailing-icon"
|
||||
className="pl-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<Form
|
||||
method="post"
|
||||
action={`/resources/orgs/${organization.id}/projects/${project.id}/environments/${environment.id}/runs/bulkaction${location.search}`}
|
||||
className="w-full"
|
||||
>
|
||||
{data?.count}
|
||||
<Button LeadingIcon={XCircleIcon} type="submit" variant="danger/medium">
|
||||
Cancel X runs
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 border-t border-grid-dimmed px-2">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="tertiary/medium"
|
||||
LeadingIcon={action === "replay" ? ArrowPathIcon : XCircleIcon}
|
||||
leadingIconClassName={cn(
|
||||
"w-[1.3rem] h-[1.3rem]",
|
||||
action === "replay" ? "text-blue-400" : "text-error"
|
||||
)}
|
||||
>
|
||||
{action === "replay" ? "Replay" : "Cancel"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Form method="post" action={`/resources/branches/archive${location.search}`} {...form.props} className="w-full">
|
||||
// <input value={environment.id} {...conform.input(environmentId, { type: "hidden" })} />
|
||||
// <input
|
||||
// value={`${location.pathname}${location.search}`}
|
||||
// {...conform.input(redirectPath, { type: "hidden" })}
|
||||
// />
|
||||
// <Paragraph spacing>
|
||||
// This will <span className="text-text-bright">permanently</span> make this branch{" "}
|
||||
// <span className="text-text-bright">read-only</span>. You won't be able to trigger runs,
|
||||
// execute runs, or use the API for this branch.
|
||||
// </Paragraph>
|
||||
// <Paragraph spacing>
|
||||
// You will still be able to view the branch and its associated runs.
|
||||
// </Paragraph>
|
||||
// <Paragraph spacing>Once archived you can create a new branch with the same name.</Paragraph>
|
||||
// <FormError>{form.error}</FormError>
|
||||
// <FormButtons
|
||||
// confirmButton={
|
||||
// <Button LeadingIcon={ArchiveIcon} type="submit" variant="danger/medium">
|
||||
// Archive branch
|
||||
// </Button>
|
||||
// }
|
||||
// cancelButton={
|
||||
// <DialogClose asChild>
|
||||
// <Button variant="tertiary/medium">Cancel</Button>
|
||||
// </DialogClose>
|
||||
// }
|
||||
// />
|
||||
// </Form>
|
||||
// );
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type ClickHouse } from "@internal/clickhouse";
|
||||
import { ClickhouseQueryBuilder } from "@internal/clickhouse/dist/src/client/queryBuilder";
|
||||
import { type Tracer } from "@internal/tracing";
|
||||
import { type Logger, type LogLevel } from "@trigger.dev/core/logger";
|
||||
import { type TaskRunStatus } from "@trigger.dev/database";
|
||||
@@ -12,7 +13,7 @@ export type RunsRepositoryOptions = {
|
||||
tracer?: Tracer;
|
||||
};
|
||||
|
||||
export type ListRunsOptions = {
|
||||
export type FilterRunsOptions = {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
environmentId: string;
|
||||
@@ -30,7 +31,9 @@ export type ListRunsOptions = {
|
||||
batchId?: string;
|
||||
runFriendlyIds?: string[];
|
||||
runIds?: string[];
|
||||
//pagination
|
||||
};
|
||||
|
||||
export type ListRunsOptions = FilterRunsOptions & {
|
||||
page: {
|
||||
size: number;
|
||||
cursor?: string;
|
||||
@@ -43,77 +46,7 @@ export class RunsRepository {
|
||||
|
||||
async listRuns(options: ListRunsOptions) {
|
||||
const queryBuilder = this.options.clickhouse.taskRuns.queryBuilder();
|
||||
queryBuilder
|
||||
.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) {
|
||||
queryBuilder.where("task_identifier IN {tasks: Array(String)}", { tasks: options.tasks });
|
||||
}
|
||||
|
||||
if (options.versions && options.versions.length > 0) {
|
||||
queryBuilder.where("task_version IN {versions: Array(String)}", {
|
||||
versions: options.versions,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.statuses && options.statuses.length > 0) {
|
||||
queryBuilder.where("status IN {statuses: Array(String)}", { statuses: options.statuses });
|
||||
}
|
||||
|
||||
if (options.tags && options.tags.length > 0) {
|
||||
queryBuilder.where("hasAny(tags, {tags: Array(String)})", { tags: options.tags });
|
||||
}
|
||||
|
||||
if (options.scheduleId) {
|
||||
queryBuilder.where("schedule_id = {scheduleId: String}", { scheduleId: options.scheduleId });
|
||||
}
|
||||
|
||||
// Period is a number of milliseconds duration
|
||||
if (options.period) {
|
||||
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", {
|
||||
period: new Date(Date.now() - options.period).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 });
|
||||
}
|
||||
|
||||
if (typeof options.isTest === "boolean") {
|
||||
queryBuilder.where("is_test = {isTest: Boolean}", { isTest: options.isTest });
|
||||
}
|
||||
|
||||
if (options.rootOnly) {
|
||||
queryBuilder.where("root_run_id = ''");
|
||||
}
|
||||
|
||||
if (options.batchId) {
|
||||
queryBuilder.where("batch_id = {batchId: String}", { batchId: options.batchId });
|
||||
}
|
||||
|
||||
if (options.runFriendlyIds && options.runFriendlyIds.length > 0) {
|
||||
queryBuilder.where("friendly_id IN {runFriendlyIds: Array(String)}", {
|
||||
runFriendlyIds: options.runFriendlyIds,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.runIds && options.runIds.length > 0) {
|
||||
queryBuilder.where("run_id IN {runIds: Array(String)}", { runIds: options.runIds });
|
||||
}
|
||||
applyRunFiltersToQueryBuilder(queryBuilder, options);
|
||||
|
||||
if (options.page.cursor) {
|
||||
if (options.page.direction === "forward") {
|
||||
@@ -222,4 +155,98 @@ export class RunsRepository {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async countRuns(options: FilterRunsOptions) {
|
||||
const queryBuilder = this.options.clickhouse.taskRuns.countQueryBuilder();
|
||||
applyRunFiltersToQueryBuilder(queryBuilder, options);
|
||||
|
||||
const [queryError, result] = await queryBuilder.execute();
|
||||
|
||||
if (queryError) {
|
||||
throw queryError;
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
throw new Error("No count rows returned");
|
||||
}
|
||||
|
||||
return result[0].count;
|
||||
}
|
||||
}
|
||||
|
||||
function applyRunFiltersToQueryBuilder<T>(
|
||||
queryBuilder: ClickhouseQueryBuilder<T>,
|
||||
options: FilterRunsOptions
|
||||
) {
|
||||
queryBuilder
|
||||
.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) {
|
||||
queryBuilder.where("task_identifier IN {tasks: Array(String)}", { tasks: options.tasks });
|
||||
}
|
||||
|
||||
if (options.versions && options.versions.length > 0) {
|
||||
queryBuilder.where("task_version IN {versions: Array(String)}", {
|
||||
versions: options.versions,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.statuses && options.statuses.length > 0) {
|
||||
queryBuilder.where("status IN {statuses: Array(String)}", { statuses: options.statuses });
|
||||
}
|
||||
|
||||
if (options.tags && options.tags.length > 0) {
|
||||
queryBuilder.where("hasAny(tags, {tags: Array(String)})", { tags: options.tags });
|
||||
}
|
||||
|
||||
if (options.scheduleId) {
|
||||
queryBuilder.where("schedule_id = {scheduleId: String}", { scheduleId: options.scheduleId });
|
||||
}
|
||||
|
||||
// Period is a number of milliseconds duration
|
||||
if (options.period) {
|
||||
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", {
|
||||
period: new Date(Date.now() - options.period).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 });
|
||||
}
|
||||
|
||||
if (typeof options.isTest === "boolean") {
|
||||
queryBuilder.where("is_test = {isTest: Boolean}", { isTest: options.isTest });
|
||||
}
|
||||
|
||||
if (options.rootOnly) {
|
||||
queryBuilder.where("root_run_id = ''");
|
||||
}
|
||||
|
||||
if (options.batchId) {
|
||||
queryBuilder.where("batch_id = {batchId: String}", { batchId: options.batchId });
|
||||
}
|
||||
|
||||
if (options.runFriendlyIds && options.runFriendlyIds.length > 0) {
|
||||
queryBuilder.where("friendly_id IN {runFriendlyIds: Array(String)}", {
|
||||
runFriendlyIds: options.runFriendlyIds,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.runIds && options.runIds.length > 0) {
|
||||
queryBuilder.where("run_id IN {runIds: Array(String)}", { runIds: options.runIds });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getCurrentRunningStats,
|
||||
getAverageDurations,
|
||||
getTaskUsageByOrganization,
|
||||
getTaskRunsCountQueryBuilder,
|
||||
} from "./taskRuns.js";
|
||||
import { Logger, type LogLevel } from "@trigger.dev/core/logger";
|
||||
import type { Agent as HttpAgent } from "http";
|
||||
@@ -144,6 +145,7 @@ export class ClickHouse {
|
||||
insert: insertTaskRuns(this.writer),
|
||||
insertPayloads: insertRawTaskRunPayloads(this.writer),
|
||||
queryBuilder: getTaskRunsQueryBuilder(this.reader),
|
||||
countQueryBuilder: getTaskRunsCountQueryBuilder(this.reader),
|
||||
getTaskActivity: getTaskActivityQueryBuilder(this.reader),
|
||||
getCurrentRunningStats: getCurrentRunningStats(this.reader),
|
||||
getAverageDurations: getAverageDurations(this.reader),
|
||||
|
||||
@@ -103,6 +103,17 @@ export function getTaskRunsQueryBuilder(ch: ClickhouseReader, settings?: ClickHo
|
||||
});
|
||||
}
|
||||
|
||||
export function getTaskRunsCountQueryBuilder(ch: ClickhouseReader, settings?: ClickHouseSettings) {
|
||||
return ch.queryBuilder({
|
||||
name: "getTaskRunsCount",
|
||||
baseQuery: "SELECT count() as count FROM trigger_dev.task_runs_v2 FINAL",
|
||||
schema: z.object({
|
||||
count: z.number().int(),
|
||||
}),
|
||||
settings,
|
||||
});
|
||||
}
|
||||
|
||||
export const TaskActivityQueryResult = z.object({
|
||||
task_identifier: z.string(),
|
||||
status: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user