Skip to content

Commit 15c6760

Browse files
committed
Filter summary in the bulk inspector
1 parent 464acdd commit 15c6760

File tree

4 files changed

+285
-226
lines changed

4 files changed

+285
-226
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { z } from "zod";
2+
import {
3+
filterIcon,
4+
filterTitle,
5+
type TaskRunListSearchFilterKey,
6+
type TaskRunListSearchFilters,
7+
} from "./runs/v3/RunFilters";
8+
import { Paragraph } from "./primitives/Paragraph";
9+
import simplur from "simplur";
10+
import { appliedSummary, dateFromString, timeFilterRenderValues } from "./runs/v3/SharedFilters";
11+
import { formatNumber } from "~/utils/numberFormatter";
12+
import { SpinnerWhite } from "./primitives/Spinner";
13+
import { ArrowPathIcon, CheckIcon, XCircleIcon } from "@heroicons/react/20/solid";
14+
import assertNever from "assert-never";
15+
import { AppliedFilter } from "./primitives/AppliedFilter";
16+
import { runStatusTitle } from "./runs/v3/TaskRunStatus";
17+
import { type TaskRunStatus } from "@trigger.dev/database";
18+
19+
export const BulkActionMode = z.union([z.literal("selected"), z.literal("filter")]);
20+
export type BulkActionMode = z.infer<typeof BulkActionMode>;
21+
export const BulkActionAction = z.union([z.literal("cancel"), z.literal("replay")]);
22+
export type BulkActionAction = z.infer<typeof BulkActionAction>;
23+
24+
export function BulkActionFilterSummary({
25+
selected,
26+
final = false,
27+
mode,
28+
action,
29+
filters,
30+
}: {
31+
selected?: number;
32+
final?: boolean;
33+
mode: BulkActionMode;
34+
action: BulkActionAction;
35+
filters: TaskRunListSearchFilters;
36+
}) {
37+
switch (mode) {
38+
case "selected":
39+
return (
40+
<Paragraph variant="small">
41+
You {!final ? "have " : " "}individually selected {simplur`${selected} run[|s]`} to be{" "}
42+
<Action action={action} />.
43+
</Paragraph>
44+
);
45+
case "filter": {
46+
const { label, valueLabel, rangeType } = timeFilterRenderValues({
47+
from: filters.from ? dateFromString(`${filters.from}`) : undefined,
48+
to: filters.to ? dateFromString(`${filters.to}`) : undefined,
49+
period: filters.period,
50+
});
51+
52+
return (
53+
<div className="flex flex-col gap-2">
54+
<Paragraph variant="small">
55+
You {!final ? "have " : " "}selected{" "}
56+
<span className="text-text-bright">
57+
{final ? selected : <EstimatedCount count={selected} />}
58+
</span>{" "}
59+
runs to be <Action action={action} /> using these filters:
60+
</Paragraph>
61+
<div className="flex flex-col gap-2">
62+
<AppliedFilter
63+
variant="minimal/medium"
64+
label={label}
65+
icon={filterIcon("period")}
66+
value={valueLabel}
67+
removable={false}
68+
/>
69+
{Object.entries(filters).map(([key, value]) => {
70+
if (!value && key !== "period") {
71+
return null;
72+
}
73+
74+
const typedKey = key as TaskRunListSearchFilterKey;
75+
76+
switch (typedKey) {
77+
case "cursor":
78+
case "direction":
79+
case "environments":
80+
//We need to handle time differently because we have a default
81+
case "period":
82+
case "from":
83+
case "to": {
84+
return null;
85+
}
86+
case "tasks": {
87+
const values = Array.isArray(value) ? value : [`${value}`];
88+
return (
89+
<AppliedFilter
90+
variant="minimal/medium"
91+
key={key}
92+
label={filterTitle(key)}
93+
icon={filterIcon(key)}
94+
value={appliedSummary(values)}
95+
removable={false}
96+
/>
97+
);
98+
}
99+
case "versions": {
100+
const values = Array.isArray(value) ? value : [`${value}`];
101+
return (
102+
<AppliedFilter
103+
variant="minimal/medium"
104+
key={key}
105+
label={filterTitle(key)}
106+
icon={filterIcon(key)}
107+
value={appliedSummary(values)}
108+
removable={false}
109+
/>
110+
);
111+
}
112+
case "statuses": {
113+
const values = Array.isArray(value) ? value : [`${value}`];
114+
return (
115+
<AppliedFilter
116+
variant="minimal/medium"
117+
key={key}
118+
label={filterTitle(key)}
119+
icon={filterIcon(key)}
120+
value={appliedSummary(values.map((v) => runStatusTitle(v as TaskRunStatus)))}
121+
removable={false}
122+
/>
123+
);
124+
}
125+
case "tags": {
126+
const values = Array.isArray(value) ? value : [`${value}`];
127+
return (
128+
<AppliedFilter
129+
variant="minimal/medium"
130+
key={key}
131+
label={filterTitle(key)}
132+
icon={filterIcon(key)}
133+
value={appliedSummary(values)}
134+
removable={false}
135+
/>
136+
);
137+
}
138+
case "bulkId": {
139+
return (
140+
<AppliedFilter
141+
variant="minimal/medium"
142+
key={key}
143+
label={filterTitle(key)}
144+
icon={filterIcon(key)}
145+
value={value}
146+
removable={false}
147+
/>
148+
);
149+
}
150+
case "rootOnly": {
151+
return (
152+
<AppliedFilter
153+
variant="minimal/medium"
154+
key={key}
155+
label={"Root only"}
156+
icon={filterIcon(key)}
157+
value={
158+
value ? (
159+
<CheckIcon className="size-4" />
160+
) : (
161+
<XCircleIcon className="size-4" />
162+
)
163+
}
164+
removable={false}
165+
/>
166+
);
167+
}
168+
case "runId": {
169+
return (
170+
<AppliedFilter
171+
variant="minimal/medium"
172+
key={key}
173+
label={"Run ID"}
174+
icon={filterIcon(key)}
175+
value={value}
176+
removable={false}
177+
/>
178+
);
179+
}
180+
case "batchId": {
181+
return (
182+
<AppliedFilter
183+
variant="minimal/medium"
184+
key={key}
185+
label={"Batch ID"}
186+
icon={filterIcon(key)}
187+
value={value}
188+
removable={false}
189+
/>
190+
);
191+
}
192+
case "scheduleId": {
193+
return (
194+
<AppliedFilter
195+
variant="minimal/medium"
196+
key={key}
197+
label={"Schedule ID"}
198+
icon={filterIcon(key)}
199+
value={value}
200+
removable={false}
201+
/>
202+
);
203+
}
204+
default: {
205+
assertNever(typedKey);
206+
}
207+
}
208+
})}
209+
</div>
210+
</div>
211+
);
212+
}
213+
}
214+
}
215+
216+
function Action({ action }: { action: BulkActionAction }) {
217+
switch (action) {
218+
case "cancel":
219+
return (
220+
<span>
221+
<XCircleIcon className="mb-0.5 inline-block size-4 text-error" />
222+
<span className="ml-0.5 text-text-bright">Canceled</span>
223+
</span>
224+
);
225+
case "replay":
226+
return (
227+
<span>
228+
<ArrowPathIcon className="mb-0.5 inline-block size-4 text-blue-400" />
229+
<span className="ml-0.5 text-text-bright">Replayed</span>
230+
</span>
231+
);
232+
}
233+
}
234+
235+
export function EstimatedCount({ count }: { count?: number }) {
236+
if (typeof count === "number") {
237+
return <>~{formatNumber(count)}</>;
238+
}
239+
240+
return <SpinnerWhite className="mx-0.5 -mt-0.5 inline size-3" />;
241+
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { getUsername } from "~/utils/username";
22
import { BasePresenter } from "./basePresenter.server";
3+
import { type BulkActionMode } from "~/components/BulkActionFilterSummary";
4+
import { parseRunListInputOptions } from "~/services/runsRepository.server";
5+
import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
36

47
type BulkActionOptions = {
58
environmentId: string;
@@ -26,6 +29,13 @@ export class BulkActionPresenter extends BasePresenter {
2629
avatarUrl: true,
2730
},
2831
},
32+
params: true,
33+
project: {
34+
select: {
35+
id: true,
36+
organizationId: true,
37+
},
38+
},
2939
},
3040
where: {
3141
environmentId,
@@ -37,11 +47,23 @@ export class BulkActionPresenter extends BasePresenter {
3747
throw new Error("Bulk action not found");
3848
}
3949

50+
//parse filters
51+
const filtersParsed = TaskRunListSearchFilters.safeParse(
52+
bulkAction.params && typeof bulkAction.params === "object" ? bulkAction.params : {}
53+
);
54+
55+
let mode: BulkActionMode = "filter";
56+
if (filtersParsed.success && Object.keys(filtersParsed.data).length === 0) {
57+
mode = "selected";
58+
}
59+
4060
return {
4161
...bulkAction,
4262
user: bulkAction.user
4363
? { name: getUsername(bulkAction.user), avatarUrl: bulkAction.user.avatarUrl }
4464
: undefined,
65+
filters: filtersParsed.data ?? {},
66+
mode,
4567
};
4668
}
4769
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ArrowPathIcon } from "@heroicons/react/20/solid";
22
import { Form, useRevalidator } from "@remix-run/react";
3-
import { ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
3+
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
44
import { tryCatch } from "@trigger.dev/core";
55
import { BulkActionStatus, BulkActionType } from "@trigger.dev/database";
66
import { motion } from "framer-motion";
@@ -9,13 +9,13 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
99
import { z } from "zod";
1010
import { ExitIcon } from "~/assets/icons/ExitIcon";
1111
import { RunsIcon } from "~/assets/icons/RunsIcon";
12+
import { BulkActionFilterSummary } from "~/components/BulkActionFilterSummary";
1213
import { Button, LinkButton } from "~/components/primitives/Buttons";
1314
import { CopyableText } from "~/components/primitives/CopyableText";
1415
import { DateTime } from "~/components/primitives/DateTime";
1516
import { Header2 } from "~/components/primitives/Headers";
1617
import { Paragraph } from "~/components/primitives/Paragraph";
1718
import * as Property from "~/components/primitives/PropertyTable";
18-
import { SimpleTooltip } from "~/components/primitives/Tooltip";
1919
import { BulkActionStatusCombo, BulkActionTypeCombo } from "~/components/runs/v3/BulkAction";
2020
import { UserAvatar } from "~/components/UserProfilePhoto";
2121
import { useEnvironment } from "~/hooks/useEnvironment";
@@ -233,6 +233,18 @@ export default function Page() {
233233
{bulkAction.completedAt ? <DateTime date={bulkAction.completedAt} /> : "–"}
234234
</Property.Value>
235235
</Property.Item>
236+
<Property.Item>
237+
<Property.Label>Summary</Property.Label>
238+
<Property.Value>
239+
<BulkActionFilterSummary
240+
selected={bulkAction.totalCount}
241+
mode={bulkAction.mode}
242+
action={bulkAction.type === BulkActionType.REPLAY ? "replay" : "cancel"}
243+
filters={bulkAction.filters}
244+
final={true}
245+
/>
246+
</Property.Value>
247+
</Property.Item>
236248
</Property.Table>
237249
</div>
238250
</div>

0 commit comments

Comments
 (0)