Skip to content
Open
Show file tree
Hide file tree
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
26 changes: 26 additions & 0 deletions report-app/src/app/pages/report-viewer/report-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@ <h3 class="chart-title">
<stacked-bar-chart [data]="buildsAsGraphData(overview.stats.builds)" [compact]="true" />
</div>
</div>
@if (hasSuccessfulResultWithMoreThanOneBuildAttempt()) {
<div class="chart-container repair-attempts">
<h3>
<span class="material-symbols-outlined">build_circle</span>
<span>Repair attempts</span>
<span
class="material-symbols-outlined has-tooltip multiline-tooltip chart-title-tooltip-icon"
data-tooltip="For applications that required repairs to be built, this displays the distribution of how many repair attempts were required."
>info</span
>
@if (averageRepairAttempts() !== null) {
<span class="chart-title-right-label"
>Avg: {{ averageRepairAttempts() | number: '1.2-2' }}</span
>
<span
class="material-symbols-outlined has-tooltip multiline-tooltip chart-title-tooltip-icon"
data-tooltip="Average repair count among applications that were successfully built after repairs."
>info</span
>
}
</h3>
<div class="summary-card-item">
<stacked-bar-chart [data]="repairAttemptsAsGraphData()" [compact]="true" />
</div>
</div>
}
@if (overview.stats.tests) {
<div class="chart-container test-results-details">
<h3 class="chart-title">
Expand Down
11 changes: 11 additions & 0 deletions report-app/src/app/pages/report-viewer/report-viewer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ lighthouse-category + lighthouse-category {
align-items: center;
}

.chart-title-tooltip-icon {
font-size: 18px;
cursor: help;
}

.chart-title-right-label {
margin-left: auto;
font-size: 0.9rem;
font-weight: 500;
}

.axe-violations ul {
padding: 0px 20px;
}
Expand Down
115 changes: 114 additions & 1 deletion report-app/src/app/pages/report-viewer/report-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
viewChild,
} from '@angular/core';
import {NgxJsonViewerModule} from 'ngx-json-viewer';
import {BuildErrorType} from '../../../../../runner/workers/builder/builder-types';
import {
BuildErrorType,
BuildResultStatus,
} from '../../../../../runner/workers/builder/builder-types';
import {
AssessmentResult,
AssessmentResultFromReportServer,
Expand Down Expand Up @@ -281,6 +284,116 @@ export class ReportViewer {
];
}

protected hasSuccessfulResultWithMoreThanOneBuildAttempt = computed(() => {
if (!this.selectedReport.hasValue()) {
return false;
}
for (const result of this.selectedReport.value().results) {
if (
result.finalAttempt.buildResult.status === BuildResultStatus.SUCCESS &&
result.repairAttempts > 1
) {
return true;
}
}
return false;
});

protected averageRepairAttempts = computed<number | null>(() => {
const report = this.selectedReportWithSortedResults();
if (!report) {
return null;
}

let totalRepairs = 0;
let count = 0;

for (const result of report.results) {
// Only consider successful builds that required repairs.
if (
result.finalAttempt.buildResult.status === BuildResultStatus.SUCCESS &&
result.repairAttempts > 0
) {
totalRepairs += result.repairAttempts;
count++;
}
}

return count > 0 ? totalRepairs / count : null;
});

protected repairAttemptsAsGraphData = computed<StackedBarChartData>(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose moving most of the logic from this function (and maybe also the function above) to a new function outside of this class (to keep the class as simple as possible).

const report = this.selectedReportWithSortedResults();
if (!report) {
return [];
}

const repairsToAppCount = new Map<number | 'failed', number>();

// Map repair count to how many applications shared that count.
let maxRepairCount = 0;
for (const result of report.results) {
if (result.finalAttempt.buildResult.status === BuildResultStatus.ERROR) {
repairsToAppCount.set('failed', (repairsToAppCount.get('failed') || 0) + 1);
} else {
const repairs = result.repairAttempts;
// For this graph, we ignore applications that required no repair.
if (repairs > 0) {
repairsToAppCount.set(repairs, (repairsToAppCount.get(repairs) || 0) + 1);
maxRepairCount = Math.max(maxRepairCount, repairs);
}
}
}

const data: StackedBarChartData = [];

// All the numeric keys, sorted by value.
const intermediateRepairKeys = Array.from(repairsToAppCount.keys())
.filter((k): k is number => typeof k === 'number')
.sort((a, b) => a - b);

// This graph might involve a bunch of sections. We want to scale them among all the possible color "grades".

const minGrade = 1;
const maxGrade = 8;
const failureGrade = 9;

for (let repairCount = 1; repairCount <= maxRepairCount; repairCount++) {
const applicationCount = repairsToAppCount.get(repairCount);
if (!applicationCount) continue;
const label = `${repairCount} repair${repairCount > 1 ? 's' : ''}`;

// Normalize the repair count to the range [0, 1].
const normalizedRepairCount = (repairCount - 1) / (maxRepairCount - 1);

let gradeIndex: number;
if (intermediateRepairKeys.length === 1) {
// If there's only one intermediate repair count, map it to a middle grade (e.g., --chart-grade-5)
gradeIndex = Math.floor(maxGrade / 2) + minGrade;
} else {
// Distribute multiple intermediate repair counts evenly across available grades
gradeIndex = minGrade + Math.round(normalizedRepairCount * (maxGrade - minGrade));
}

data.push({
label,
color: `var(--chart-grade-${gradeIndex})`,
value: applicationCount,
});
}

// Handle 'Build failed even after all retries' - always maps to the "failure" grade.
const failedCount = repairsToAppCount.get('failed') || 0;
if (failedCount > 0) {
data.push({
label: 'Build failed even after all retries',
color: `var(--chart-grade-${failureGrade})`,
value: failedCount,
});
}
return data;
});

protected testsAsGraphData(tests: RunSummaryTests): StackedBarChartData {
return [
{
Expand Down
1 change: 1 addition & 0 deletions report-app/src/app/shared/styles/tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

&.multiline-tooltip::before {
white-space: normal;
width: max-content;
max-width: 400px;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@

.legend {
display: flex;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
justify-content: flex-start;
column-gap: 1.5rem;
}

.legend-item {
Expand All @@ -66,6 +67,7 @@
font-size: 14px;
color: var(--text-secondary);
white-space: nowrap;
margin-top: 0.5rem;
}

.legend-color {
Expand Down
11 changes: 11 additions & 0 deletions report-app/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@
--status-text-poor: #eb1515;
--status-text-neutral: #64748b;

/* 10-step Green-to-Red Quality Gradient */
--chart-grade-1: #10b981; /* Emerald 500 (Excellent) */
--chart-grade-2: #22c55e; /* Green 500 */
--chart-grade-3: #4ade80; /* Green 400 */
--chart-grade-4: #84cc16; /* Lime 500 (Great) */
Comment on lines +42 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid using green colors for this graph. We can also reduce the number of attempts that we render individually to 5 and collect the remaining ones into a separate bucket (and use the "over 5 attempts" text for it).

--chart-grade-5: #a3e635; /* Lime 400 */
--chart-grade-6: #facc15; /* Yellow 400 */
--chart-grade-7: #f59e0b; /* Amber 500 (Good) */
--chart-grade-8: #f97316; /* Orange 500 */
--chart-grade-9: #ef4444; /* Red 500 (Poor) */

--tooltip-background-color: light-dark(#111827, #f1f4f9);
--tooltip-text-color: light-dark(#f9fafb, #1e293b);

Expand Down
Loading