Skip to content

Commit 1997b69

Browse files
authored
[HUD] Add job-name filter to commit-page Gantt chart (pytorch#8329)
Add a filter input to WorkflowGantt so a workflow box with many jobs (pull now has 140+) can be narrowed to just the jobs of interest for comparison. Plain case-insensitive substring match on the job name. This helps with balancing the visuals and drilling down specific job types. The filter is a row-visibility mask only: the time axis is still derived from all jobs, so filtering does not reorient the timeline — matched bars keep their exact positions. **Test Plan:** - Check `/ build` + `linux-jammy-py3.14-clang18 / test (default,` filter to expected targets - Use non-existent filter - Compare A/B page that metrics are retained and correct
1 parent 93094f7 commit 1997b69

1 file changed

Lines changed: 46 additions & 7 deletions

File tree

torchci/components/commit/JobTimeline.tsx

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
169169
const data = useMemo(() => processJobs(jobs), [jobs]);
170170
const [colorMode, setColorMode] = useState<"group" | "status">("status");
171171
const [locked, setLocked] = useState(false);
172+
const [filter, setFilter] = useState("");
172173
const [containerRef, containerWidth] = useContainerWidth();
173174
const theme = useTheme();
174175
// Chrome/neutral colors must work on light and dark backgrounds (torchci
@@ -192,8 +193,17 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
192193
}
193194

194195
const { rows, maxEnd, skipped } = data;
195-
const nFail = rows.filter((r) => r.status === "failure").length;
196+
// Built from ALL jobs so a job keeps its color when the list is filtered.
196197
const colorMap = buildColorMap(rows.map((r) => r.groupKey));
198+
// Row-visibility mask only: the time axis stays derived from all jobs (below),
199+
// so filtering never reorients the timeline. Plain substring, not regex.
200+
// Match the displayed row label (the prefix-less job name). full (=name) only
201+
// adds the constant "<workflow> / " prefix, so it can't narrow within a box.
202+
const query = filter.trim().toLowerCase();
203+
const visibleRows = query
204+
? rows.filter((r) => r.label.toLowerCase().includes(query))
205+
: rows;
206+
const nFail = visibleRows.filter((r) => r.status === "failure").length;
197207

198208
// px/minute: fit the (measured) bars pane by default, fixed when locked. The
199209
// plot keeps a readable minimum and scrolls horizontally instead of squishing.
@@ -203,7 +213,7 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
203213

204214
const plotW = maxEnd * pxmin;
205215
const W = PAD_L + plotW + RIGHT;
206-
const H = AXIS + rows.length * ROW + 8;
216+
const H = AXIS + visibleRows.length * ROW + 8;
207217
const xat = (m: number) => PAD_L + m * pxmin;
208218

209219
const targetTicks = Math.max(2, Math.round(plotW / 100));
@@ -219,15 +229,15 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
219229
// legend entries for current mode
220230
const legend =
221231
colorMode === "group"
222-
? Array.from(new Set(rows.map((r) => r.groupKey)))
232+
? Array.from(new Set(visibleRows.map((r) => r.groupKey)))
223233
.sort((a, b) =>
224234
a === "build" ? -1 : b === "build" ? 1 : a.localeCompare(b)
225235
)
226236
.map((k) => ({
227237
color: colorMap[k],
228238
label: k === "build" ? "build" : shortName(k),
229239
}))
230-
: Array.from(new Set(rows.map((r) => r.status))).map((s) => ({
240+
: Array.from(new Set(visibleRows.map((r) => r.status))).map((s) => ({
231241
color: statusColor(s, neutralColor),
232242
label: s,
233243
}));
@@ -252,7 +262,10 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
252262
}}
253263
>
254264
<span style={{ color: muted }}>
255-
{rows.length} jobs · {Math.round(maxEnd)} min
265+
{query
266+
? `${visibleRows.length} of ${rows.length} jobs`
267+
: `${rows.length} jobs`}{" "}
268+
· {Math.round(maxEnd)} min
256269
{nFail > 0 && (
257270
<span style={{ color: "#e03b3b" }}> · {nFail} failing</span>
258271
)}
@@ -285,8 +298,34 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
285298
/>{" "}
286299
Lock scale
287300
</label>
301+
<span
302+
style={{ display: "inline-flex", alignItems: "center", gap: 4 }}
303+
title="Show only jobs whose name contains this text (case-insensitive substring). The timeline scale is unchanged — matched bars keep their positions."
304+
>
305+
<span style={{ color: muted }}>Filter:</span>
306+
<input
307+
type="text"
308+
value={filter}
309+
onChange={(e) => setFilter(e.target.value)}
310+
placeholder="filter by job name"
311+
aria-label="Filter jobs by name"
312+
style={{
313+
fontSize: 12,
314+
padding: "2px 6px",
315+
width: 200,
316+
border: `1px solid ${borderColor}`,
317+
borderRadius: 4,
318+
}}
319+
/>
320+
</span>
288321
</div>
289322

323+
{query && visibleRows.length === 0 && (
324+
<div style={{ padding: "4px 10px", fontSize: 12, color: muted }}>
325+
No jobs match “{filter.trim()}”.
326+
</div>
327+
)}
328+
290329
<div style={{ display: "flex", alignItems: "flex-start" }}>
291330
{/* Label column: fixed width. Scrolls horizontally ONLY when a job name
292331
is longer than the column, so one long name doesn't widen everything. */}
@@ -299,7 +338,7 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
299338
}}
300339
>
301340
<div style={{ height: AXIS }} />
302-
{rows.map((j, i) => (
341+
{visibleRows.map((j, i) => (
303342
<div
304343
key={i}
305344
title={j.full}
@@ -353,7 +392,7 @@ export default function WorkflowGantt({ jobs }: { jobs: JobData[] }) {
353392
</text>
354393
</g>
355394
))}
356-
{rows.map((j, i) => {
395+
{visibleRows.map((j, i) => {
357396
const y = AXIS + i * ROW;
358397
const bh = ROW - 4;
359398
const qw = Math.max((j.so - j.co) * pxmin, 0);

0 commit comments

Comments
 (0)