Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

monsterized failures in grouped view #6394

Merged
merged 3 commits into from
Mar 12, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 180 additions & 18 deletions torchci/components/GroupJobConclusion.tsx
Original file line number Diff line number Diff line change
@@ -6,9 +6,13 @@ import {
isUnstableJob,
} from "lib/jobUtils";
import { IssueData, JobData } from "lib/types";
import { PinnedTooltipContext } from "pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]]";
import {
MonsterFailuresContext,
PinnedTooltipContext,
} from "pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]]";
import { useContext } from "react";
import hudStyles from "./hud.module.css";
import { getFailureEl } from "./JobConclusion";
import styles from "./JobConclusion.module.css";
import { SingleWorkflowDispatcher } from "./WorkflowDispatcher";

@@ -65,6 +69,94 @@ function isJobViableStrictBlocking(
return false;
}

// React component to render either a group conclusion character or monsterized icons for failures
function GroupConclusionContent({
conclusion,
isClassified,
erroredJobs,
toggleExpanded,
monsterFailures,
}: {
conclusion: GroupedJobStatus;
isClassified: boolean;
erroredJobs: JobData[];
toggleExpanded: () => void;
monsterFailures: boolean;
}) {
// Only show monsters for failures and when monsterized failures is enabled
if (conclusion !== GroupedJobStatus.Failure || !monsterFailures) {
return (
<span
className={
isClassified ? styles["classified"] : styles[conclusion ?? "none"]
}
onDoubleClick={toggleExpanded}
style={{
border: "1px solid gainsboro",
padding: "0 1px",
}}
>
{getGroupConclusionChar(conclusion)}
</span>
);
}

// Get only unique monster icons based on their sprite index
const seenMonsterSprites = new Set();
const allMonsters = [];

for (const job of erroredJobs) {
if (job.failureLines && job.failureLines[0]) {
const monsterEl = getFailureEl(JobStatus.Failure, job);
if (monsterEl) {
// Get the sprite index from the data attribute
const spriteIdx = monsterEl.props["data-monster-hash"];

if (!seenMonsterSprites.has(spriteIdx)) {
seenMonsterSprites.add(spriteIdx);
allMonsters.push(monsterEl);
}
}
}
}

if (allMonsters.length === 0) {
// Fallback to X if no monsters could be generated
return (
<span
className={
isClassified ? styles["classified"] : styles[conclusion ?? "none"]
}
onDoubleClick={toggleExpanded}
style={{
border: "1px solid gainsboro",
padding: "0 1px",
}}
>
{getGroupConclusionChar(conclusion)}
</span>
);
}

// Show the first monster icon with a count in bottom right
const firstMonster = allMonsters[0];

return (
<span
className={styles.monster_with_count}
onDoubleClick={toggleExpanded}
title={`${allMonsters.length} unique failure ${
allMonsters.length === 1 ? "type" : "types"
}`}
>
{firstMonster}
{allMonsters.length > 1 && (
<span className={styles.monster_count}>{allMonsters.length}</span>
)}
</span>
);
}

export default function HudGroupedCell({
sha,
groupName,
@@ -87,6 +179,7 @@ export default function HudGroupedCell({
repoName: string;
}) {
const [pinnedId, setPinnedId] = useContext(PinnedTooltipContext);
const [monsterFailures] = useContext(MonsterFailuresContext);
const style = pinnedId.name == groupName ? hudStyles.highlight : "";

const erroredJobs = [];
@@ -153,26 +246,38 @@ export default function HudGroupedCell({
/>
}
>
<span
className={`${styles.conclusion} ${
viableStrictBlocking ? styles.viablestrict_blocking : ""
}`}
>
{monsterFailures && conclusion === GroupedJobStatus.Failure ? (
<span className={styles.conclusion}>
<span
className={
viableStrictBlocking ? styles.viablestrict_blocking : ""
}
style={{ padding: "0 1px" }}
>
<GroupConclusionContent
conclusion={conclusion}
isClassified={isClassified}
erroredJobs={erroredJobs}
toggleExpanded={toggleExpanded}
monsterFailures={monsterFailures}
/>
</span>
</span>
) : (
<span
className={
isClassified
? styles["classified"]
: styles[conclusion ?? "none"]
}
onDoubleClick={toggleExpanded}
style={{
border: "1px solid gainsboro",
padding: "0 1px",
}}
className={`${styles.conclusion} ${
viableStrictBlocking ? styles.viablestrict_blocking : ""
}`}
>
{getGroupConclusionChar(conclusion)}
<GroupConclusionContent
conclusion={conclusion}
isClassified={isClassified}
erroredJobs={erroredJobs}
toggleExpanded={toggleExpanded}
monsterFailures={monsterFailures}
/>
</span>
</span>
)}
</TooltipTarget>
</td>
</>
@@ -196,7 +301,64 @@ function GroupTooltip({
failedPreviousRunJobs: JobData[];
sha?: string;
}) {
const [monsterFailures] = useContext(MonsterFailuresContext);

if (conclusion === GroupedJobStatus.Failure) {
// Show monster icons in the tooltip if monsterFailures is enabled
if (monsterFailures) {
// Group jobs by monster sprite index
const monsterGroups = new Map(); // Map of spriteIdx -> {monsterEl, jobs[]}

for (const job of erroredJobs) {
if (job.failureLines && job.failureLines[0]) {
const monsterEl = getFailureEl(JobStatus.Failure, job);
if (monsterEl) {
// Get the sprite index from the data attribute
const spriteIdx = monsterEl.props["data-monster-hash"];

if (!monsterGroups.has(spriteIdx)) {
monsterGroups.set(spriteIdx, { monsterEl, jobs: [] });
}

// Add this job to the group with this monster
monsterGroups.get(spriteIdx).jobs.push(job);
}
}
}

// Convert the map to an array for rendering
const monsterGroupsArray = Array.from(monsterGroups.values());

return (
<div>
{`[${conclusion}] ${groupName}`}
<div>The following jobs errored out:</div>
{monsterGroupsArray.map((group, groupIndex) => (
<div key={groupIndex} style={{ margin: "10px 0" }}>
<div style={{ display: "flex", alignItems: "center" }}>
{group.monsterEl}
<span style={{ marginLeft: "8px", fontWeight: "bold" }}>
{group.jobs.length > 1
? `${group.jobs.length} jobs with this error type:`
: "1 job with this error type:"}
</span>
</div>
{group.jobs.map((job: JobData, jobIndex: number) => (
<div
key={jobIndex}
style={{ marginLeft: "24px", marginTop: "4px" }}
>
<a href={job.htmlUrl} target="_blank" rel="noreferrer">
{job.name}
</a>
</div>
))}
</div>
))}
</div>
);
}

return (
<ToolTip
conclusion={conclusion}
27 changes: 27 additions & 0 deletions torchci/components/JobConclusion.module.css
Original file line number Diff line number Diff line change
@@ -21,6 +21,33 @@
height: 15px;
background-image: url("/failures_spritesheet.png");
background-size: 152px 426px; /* total size of the spritesheet (downscaled 1.5x) */
display: inline-block;
}

.grouped_monsters {
border: 1px solid gainsboro;
padding: 0 1px;
display: flex;
}

.grouped_monsters_no_border {
display: flex;
align-items: center;
}

.monster_with_count {
position: relative;
display: inline-block;
}

.monster_count {
position: absolute;
bottom: -1px;
right: -4px;
font-size: 8px;
color: #f44336;
font-weight: bold;
font-family: sans-serif;
}

.skipped {
3 changes: 2 additions & 1 deletion torchci/components/JobConclusion.tsx
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import styles from "./JobConclusion.module.css";
* @returns {JSX.Element} - A div element with a monster sprite as a background image.
* If the conclusion is not `JobStatus.Failure` or `jobData.failureLines` is not defined or empty, an empty object is returned.
*/
const getFailureEl = (conclusion?: string, jobData?: JobData) => {
export const getFailureEl = (conclusion?: string, jobData?: JobData) => {
if (
conclusion !== JobStatus.Failure ||
!jobData?.failureLines ||
@@ -38,6 +38,7 @@ const getFailureEl = (conclusion?: string, jobData?: JobData) => {
<div
className={styles.failure_monster}
style={/*background position*/ { backgroundPosition: `-${x}px -${y}px` }}
data-monster-hash={spriteIdx}
/>
);
};