[results feed] Add log links to try job failure results

Change-Id: I452ce94fd0709b36a3d4641d9ea8c9b996a9e63f
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/132061
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/results_feed/lib/src/components/blamelist.css b/results_feed/lib/src/components/blamelist.css
index f8c38ab..b0bfc70 100644
--- a/results_feed/lib/src/components/blamelist.css
+++ b/results_feed/lib/src/components/blamelist.css
@@ -4,6 +4,6 @@
  * BSD-style license that can be found in the LICENSE file.
  */
 
-.commit { margin-bottom: 8px; display: flex; }
+.commit { margin-bottom: 8px; }
 .comment-body { padding-left: 32px; }
 span.nowrap { white-space: nowrap; }
\ No newline at end of file
diff --git a/results_feed/lib/src/components/results_panel.dart b/results_feed/lib/src/components/results_panel.dart
index 56625fe..2c6c253 100644
--- a/results_feed/lib/src/components/results_panel.dart
+++ b/results_feed/lib/src/components/results_panel.dart
@@ -15,6 +15,7 @@
 import '../formatting.dart' as formatting;
 import '../model/commit.dart';
 import '../services/filter_service.dart';
+import '../services/try_data_service.dart';
 import 'log_component.dart';
 
 @Component(
@@ -43,9 +44,19 @@
   @Input()
   Changes changes;
 
+  /// [range] will be null if these are try results
   @Input()
   IntRange range;
 
+  /// [builds] will be null if these are CI results
+  @Input()
+  Map<int, Map<String, TryBuild>> builds;
+
+  /// A map from configurations to try builders. Null for CI results.
+  // TODO(whesse): Make lazy, fetch directly from try data service, not an input.
+  @Input()
+  Map<String, String> builders;
+
   @Input()
   Filter filter = Filter.defaultFilter;
 
@@ -64,6 +75,9 @@
     RelativePosition.OffsetTopLeft
   ];
 
+  String buildbucketID(int patchset, String configuration) =>
+      builds[patchset][builders[configuration]].buildbucketID;
+
   String approvalContent(Change change) =>
       change.approved ? formatting.checkmark : '';
 }
diff --git a/results_feed/lib/src/components/results_panel.html b/results_feed/lib/src/components/results_panel.html
index fd49d27..40d3705 100644
--- a/results_feed/lib/src/components/results_panel.html
+++ b/results_feed/lib/src/components/results_panel.html
@@ -30,21 +30,36 @@
           tooltipTarget #logs="tooltipTarget"
           class="indent pointer">
           {{approvalContent(change)}}{{change.name}}<br>
-          <material-tooltip-card
-              *ngIf="range.length > 0"
-              [for]="logs"
-              [preferredPositions]="preferredTooltipPositions">
-            <div *deferredContent>
-              <h4>Logs</h4>
-              <dart-log
-                  *ngFor="let configuration of change.configurations.configurations"
-                  [configuration]="configuration"
-                  [index]="range.end"
-                  [test]="change.name">
-              </dart-log>
-            </div>
-          </material-tooltip-card>
-        </span>
+      <material-tooltip-card
+          *ngIf="range != null"
+          [for]="logs"
+          [preferredPositions]="preferredTooltipPositions">
+        <div *deferredContent>
+          <h4>Logs</h4>
+          <dart-log
+              *ngFor="let configuration of change.configurations.configurations"
+              [configuration]="configuration"
+              [index]="range.end"
+              [test]="change.name">
+          </dart-log>
+        </div>
+      </material-tooltip-card>
+      <material-tooltip-card
+          *ngIf="builds != null"
+          [for]="logs"
+          [preferredPositions]="preferredTooltipPositions">
+        <div *deferredContent>
+          <h4>Logs</h4>
+          <div *ngFor="let configuration of change.configurations.configurations">
+            <a *ngIf="buildbucketID(change.patchset, configuration) != null"
+               target="_blank"
+               href="https://logs.chromium.org/logs/dart/buildbucket/cr-buildbucket.appspot.com/{{buildbucketID(change.patchset, configuration)}}/+/steps/test_results/0/logs/new_test_failures__logs_/0">
+              {{configuration}}
+            </a>
+          </div>
+        </div>
+      </material-tooltip-card>
+    </span>
     <div
         *ngIf="resultGroup.length > resultLimit"
         (click)="resultLimit = resultGroup.length"
diff --git a/results_feed/lib/src/components/results_selector_panel.dart b/results_feed/lib/src/components/results_selector_panel.dart
index 8b8957e..b8c4355 100644
--- a/results_feed/lib/src/components/results_selector_panel.dart
+++ b/results_feed/lib/src/components/results_selector_panel.dart
@@ -17,6 +17,7 @@
 import '../formatting.dart' as formatting;
 import '../model/commit.dart';
 import '../services/filter_service.dart';
+import '../services/try_data_service.dart';
 import 'log_component.dart';
 
 @Component(
@@ -78,9 +79,19 @@
   @Input()
   ChangeGroup commit;
 
+  /// [range] will be null if these are try results
   @Input()
   IntRange range;
 
+  /// [builds] will be null if these are CI results
+  @Input()
+  Map<int, Map<String, TryBuild>> builds;
+
+  /// A map from configurations to try builders. Null for CI results.
+  // TODO(whesse): Make lazy, fetch directly from try data service, not an input.
+  @Input()
+  Map<String, String> builders;
+
   @Input()
   Filter filter = Filter.defaultFilter;
 
@@ -121,6 +132,9 @@
     return configurations.summaries;
   }
 
+  String buildbucketID(int patchset, String configuration) =>
+      builds[patchset][builders[configuration]].buildbucketID;
+
   String approvalContent(Change change) =>
       change.approved ? formatting.checkmark : '';
 
diff --git a/results_feed/lib/src/components/results_selector_panel.html b/results_feed/lib/src/components/results_selector_panel.html
index b1202f4..6d44bf5 100644
--- a/results_feed/lib/src/components/results_selector_panel.html
+++ b/results_feed/lib/src/components/results_selector_panel.html
@@ -52,7 +52,7 @@
         {{approvalContent(change)}}{{change.name}}
       </span><br>
       <material-tooltip-card
-          *ngIf="range.length > 0"
+          *ngIf="range != null"
           [for]="logs"
           [preferredPositions]="preferredTooltipPositions">
         <div *deferredContent>
@@ -65,6 +65,21 @@
           </dart-log>
         </div>
       </material-tooltip-card>
+      <material-tooltip-card
+          *ngIf="builds != null"
+          [for]="logs"
+          [preferredPositions]="preferredTooltipPositions">
+        <div *deferredContent>
+          <h4>Logs</h4>
+          <div *ngFor="let configuration of change.configurations.configurations">
+            <a *ngIf="buildbucketID(change.patchset, configuration) != null"
+               target="_blank"
+               href="https://logs.chromium.org/logs/dart/buildbucket/cr-buildbucket.appspot.com/{{buildbucketID(change.patchset, configuration)}}/+/steps/test_results/0/logs/new_test_failures__logs_/0">
+              {{configuration}}
+            </a>
+          </div>
+        </div>
+      </material-tooltip-card>
     </span>
     <div
         *ngIf="resultGroup.length > resultLimit"
diff --git a/results_feed/lib/src/components/try_results_component.dart b/results_feed/lib/src/components/try_results_component.dart
index 1b9ecdb..83140da 100644
--- a/results_feed/lib/src/components/try_results_component.dart
+++ b/results_feed/lib/src/components/try_results_component.dart
@@ -47,6 +47,8 @@
   int cachedPatchset;
   List<Change> changes;
   List<Comment> comments;
+  Map<int, Map<String, TryBuild>> builds;
+  Map<String, String> builders;
   final IntRange emptyRange = IntRange(1, 0);
   bool updating = false;
   bool updatePending = false;
@@ -116,6 +118,13 @@
     if (review != cachedReview || patchset != cachedPatchset) {
       changes = await _tryDataService.changes(reviewInfo, patchset);
       comments = await _tryDataService.comments(reviewInfo.review);
+      builds = {
+        for (final patchset in reviewInfo.patchsets) patchset.number: {}
+      };
+      for (final build in reviewInfo.builds) {
+        builds[build.patchset][build.builder] = build;
+      }
+      builders = await _tryDataService.builders();
       comments..sort();
       cachedReview = review;
       cachedPatchset = patchset;
diff --git a/results_feed/lib/src/components/try_results_component.html b/results_feed/lib/src/components/try_results_component.html
index ab18155..bfd2c2e 100644
--- a/results_feed/lib/src/components/try_results_component.html
+++ b/results_feed/lib/src/components/try_results_component.html
@@ -7,11 +7,13 @@
   <h2 *ngIf="changeGroup.changes.isEmpty">No changed test results</h2>
   <results-panel *ngIf="!approving"
       [changes]="changeGroup.changes"
-      [range]="emptyRange">
+      [builds]="builds"
+      [builders]="builders">
   </results-panel>
   <results-selector-panel *ngIf="approving"
       [changes]="changeGroup.changes"
-      [range]="emptyRange"
+      [builds]="builds"
+      [builders]="builders"
       [selected]="selected"
       failuresOnly>
   </results-selector-panel>
diff --git a/results_feed/lib/src/model/commit.dart b/results_feed/lib/src/model/commit.dart
index 921f9f7..90a1524 100644
--- a/results_feed/lib/src/model/commit.dart
+++ b/results_feed/lib/src/model/commit.dart
@@ -163,6 +163,8 @@
       this.blamelistStartIndex,
       this.blamelistEndIndex,
       this.pinnedIndex,
+      this.review,
+      this.patchset,
       this.approved,
       this.active)
       : changesText = '$previousResult -> $result (expected $expected)',
@@ -180,6 +182,8 @@
           document.get('blamelist_start_index'),
           document.get('blamelist_end_index'),
           document.get('pinned_index'),
+          document.get('review'),
+          document.get('patchset'),
           // Old documents may not have this field.
           document.get('approved') ?? false,
           // Field is only present when true.
@@ -196,6 +200,8 @@
   final int blamelistStartIndex;
   final int blamelistEndIndex;
   int pinnedIndex;
+  final int review;
+  final int patchset;
   bool approved;
   bool active;
   final String configurationsText;
diff --git a/results_feed/lib/src/services/firestore_service.dart b/results_feed/lib/src/services/firestore_service.dart
index 134cc90..02a4dd1 100644
--- a/results_feed/lib/src/services/firestore_service.dart
+++ b/results_feed/lib/src/services/firestore_service.dart
@@ -67,15 +67,22 @@
   }
 
   Future<List<firestore.DocumentSnapshot>> fetchTryChanges(
-      int cl, int patch) async {
+      int review, int patchset) async {
     final results = app.firestore().collection('try_results');
     final firestore.QuerySnapshot snapshot = await results
-        .where('review', '==', cl)
-        .where('patchset', '==', patch)
+        .where('review', '==', review)
+        .where('patchset', '==', patchset)
         .get();
     return snapshot.docs;
   }
 
+  Future<List<firestore.DocumentSnapshot>> fetchTryBuilds(int review) async {
+    final results = app.firestore().collection('try_builds');
+    final firestore.QuerySnapshot snapshot =
+        await results.where('review', '==', review).get();
+    return snapshot.docs;
+  }
+
   Future<firestore.DocumentSnapshot> fetchComment(String id) =>
       app.firestore().collection('comments').doc(id).get();
 
diff --git a/results_feed/lib/src/services/try_data_service.dart b/results_feed/lib/src/services/try_data_service.dart
index 6efc398..03ff973 100644
--- a/results_feed/lib/src/services/try_data_service.dart
+++ b/results_feed/lib/src/services/try_data_service.dart
@@ -1,3 +1,5 @@
+import 'dart:developer';
+
 import 'package:firebase/src/firestore.dart';
 
 import '../model/comment.dart';
@@ -13,6 +15,12 @@
 
   bool get isLoggedIn => _firestoreService.isLoggedIn;
 
+  Map<String, String> _builders;
+
+  Future<Map<String, String>> builders() async {
+    return _builders ??= await _getBuilders();
+  }
+
   Future<List<Change>> changes(ReviewInfo reviewInfo, int patchset) async {
     final patchsets = reviewInfo.patchsets;
     if (patchsets.length < patchset) return [];
@@ -34,7 +42,8 @@
     final doc = await _firestoreService.fetchReviewInfo(review);
     if (doc.exists) {
       return ReviewInfo.fromDocument(doc)
-        ..setPatchsets(await _firestoreService.fetchPatchsetInfo(review));
+        ..setPatchsets(await _firestoreService.fetchPatchsetInfo(review))
+        ..setBuilds(await _firestoreService.fetchTryBuilds(review));
     } else {
       return ReviewInfo(review, "No results received yet for CL $review", []);
     }
@@ -53,12 +62,21 @@
         approve, comment, baseComment,
         tryResultIds: resultIds, review: review));
   }
+
+  Future<Map<String, String>> _getBuilders() async {
+    await _firestoreService.getFirebaseClient();
+    List<DocumentSnapshot> builderDocs =
+        await _firestoreService.fetchBuilders();
+    return Map<String, String>.fromIterable(builderDocs,
+        key: (doc) => doc.id, value: (doc) => '${doc.get('builder')}-try');
+  }
 }
 
 class ReviewInfo {
   int review;
   String title;
   List<Patchset> patchsets;
+  List<TryBuild> builds;
 
   ReviewInfo(this.review, this.title, this.patchsets);
 
@@ -69,7 +87,11 @@
   }
 
   void setPatchsets(List<DocumentSnapshot> docs) {
-    patchsets = docs.map((doc) => Patchset.fromDocument(doc)).toList();
+    patchsets = [for (final doc in docs) Patchset.fromDocument(doc)];
+  }
+
+  void setBuilds(List<DocumentSnapshot> docs) {
+    builds = [for (final doc in docs) TryBuild.fromDocument(doc)];
   }
 }
 
@@ -91,3 +113,24 @@
 
   String toString() => "Patchset($number, $patchsetGroup, $description, $kind)";
 }
+
+class TryBuild {
+  String builder;
+  int buildNumber;
+  String buildbucketID;
+  bool completed;
+  bool success;
+  int review;
+  int patchset;
+
+  TryBuild.fromDocument(DocumentSnapshot doc) {
+    final data = doc.data();
+    builder = data['builder'];
+    buildNumber = data['build_number'];
+    buildbucketID = data['buildbucket_id'];
+    completed = data['completed'];
+    success = data['success'];
+    review = data['review'];
+    patchset = data['patchset'];
+  }
+}