@@ -13,7 +13,10 @@ import {
1313 viewChild ,
1414} from '@angular/core' ;
1515import { NgxJsonViewerModule } from 'ngx-json-viewer' ;
16- import { BuildErrorType } from '../../../../../runner/workers/builder/builder-types' ;
16+ import {
17+ BuildErrorType ,
18+ BuildResultStatus ,
19+ } from '../../../../../runner/workers/builder/builder-types' ;
1720import {
1821 AssessmentResult ,
1922 AssessmentResultFromReportServer ,
@@ -283,6 +286,116 @@ export class ReportViewer {
283286 ] ;
284287 }
285288
289+ protected hasSuccessfulResultWithMoreThanOneBuildAttempt = computed ( ( ) => {
290+ if ( ! this . selectedReport . hasValue ( ) ) {
291+ return false ;
292+ }
293+ for ( const result of this . selectedReport . value ( ) . results ) {
294+ if (
295+ result . finalAttempt . buildResult . status === BuildResultStatus . SUCCESS &&
296+ result . repairAttempts > 1
297+ ) {
298+ return true ;
299+ }
300+ }
301+ return false ;
302+ } ) ;
303+
304+ protected averageRepairAttempts = computed < number | null > ( ( ) => {
305+ const report = this . selectedReportWithSortedResults ( ) ;
306+ if ( ! report ) {
307+ return null ;
308+ }
309+
310+ let totalRepairs = 0 ;
311+ let count = 0 ;
312+
313+ for ( const result of report . results ) {
314+ // Only consider successful builds that required repairs.
315+ if (
316+ result . finalAttempt . buildResult . status === BuildResultStatus . SUCCESS &&
317+ result . repairAttempts > 0
318+ ) {
319+ totalRepairs += result . repairAttempts ;
320+ count ++ ;
321+ }
322+ }
323+
324+ return count > 0 ? totalRepairs / count : null ;
325+ } ) ;
326+
327+ protected repairAttemptsAsGraphData = computed < StackedBarChartData > ( ( ) => {
328+ const report = this . selectedReportWithSortedResults ( ) ;
329+ if ( ! report ) {
330+ return [ ] ;
331+ }
332+
333+ const repairsToAppCount = new Map < number | 'failed' , number > ( ) ;
334+
335+ // Map repair count to how many applications shared that count.
336+ let maxRepairCount = 0 ;
337+ for ( const result of report . results ) {
338+ if ( result . finalAttempt . buildResult . status === BuildResultStatus . ERROR ) {
339+ repairsToAppCount . set ( 'failed' , ( repairsToAppCount . get ( 'failed' ) || 0 ) + 1 ) ;
340+ } else {
341+ const repairs = result . repairAttempts ;
342+ // For this graph, we ignore applications that required no repair.
343+ if ( repairs > 0 ) {
344+ repairsToAppCount . set ( repairs , ( repairsToAppCount . get ( repairs ) || 0 ) + 1 ) ;
345+ maxRepairCount = Math . max ( maxRepairCount , repairs ) ;
346+ }
347+ }
348+ }
349+
350+ const data : StackedBarChartData = [ ] ;
351+
352+ // All the numeric keys, sorted by value.
353+ const intermediateRepairKeys = Array . from ( repairsToAppCount . keys ( ) )
354+ . filter ( ( k ) : k is number => typeof k === 'number' )
355+ . sort ( ( a , b ) => a - b ) ;
356+
357+ // This graph might involve a bunch of sections. We want to scale them among all the possible color "grades".
358+
359+ const minGrade = 1 ;
360+ const maxGrade = 8 ;
361+ const failureGrade = 9 ;
362+
363+ for ( let repairCount = 1 ; repairCount <= maxRepairCount ; repairCount ++ ) {
364+ const applicationCount = repairsToAppCount . get ( repairCount ) ;
365+ if ( ! applicationCount ) continue ;
366+ const label = `${ repairCount } repair${ repairCount > 1 ? 's' : '' } ` ;
367+
368+ // Normalize the repair count to the range [0, 1].
369+ const normalizedRepairCount = ( repairCount - 1 ) / ( maxRepairCount - 1 ) ;
370+
371+ let gradeIndex : number ;
372+ if ( intermediateRepairKeys . length === 1 ) {
373+ // If there's only one intermediate repair count, map it to a middle grade (e.g., --chart-grade-5)
374+ gradeIndex = Math . floor ( maxGrade / 2 ) + minGrade ;
375+ } else {
376+ // Distribute multiple intermediate repair counts evenly across available grades
377+ gradeIndex = minGrade + Math . round ( normalizedRepairCount * ( maxGrade - minGrade ) ) ;
378+ }
379+
380+ data . push ( {
381+ label,
382+ color : `var(--chart-grade-${ gradeIndex } )` ,
383+ value : applicationCount ,
384+ } ) ;
385+ }
386+
387+ // Handle 'Build failed even after all retries' - always maps to the "failure" grade.
388+ const failedCount = repairsToAppCount . get ( 'failed' ) || 0 ;
389+ if ( failedCount > 0 ) {
390+ data . push ( {
391+ label : 'Build failed even after all retries' ,
392+ color : `var(--chart-grade-${ failureGrade } )` ,
393+ value : failedCount ,
394+ } ) ;
395+ }
396+ return data ;
397+ } ) ;
398+
286399 protected testsAsGraphData ( tests : RunSummaryTests ) : StackedBarChartData {
287400 return [
288401 {
0 commit comments