Scouting App: View Data Interface
Add a tab to the scouting app to view all data collected by the
scouting app including notes, stats, and driver ranking.
You can sort data by ascending/descending match or team numbers.
In the future, this tab should be intergrated with the scouting
app tests to verify if data was actually submitted and a delete
option should be added so that scouts can delete and resubmit data
if necessary.
Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
Change-Id: Idc8938f2c309a79f0006c6c99781ca06a506a19d
diff --git a/scouting/www/index.template.html b/scouting/www/index.template.html
index 303dca1..8fd0d7c 100644
--- a/scouting/www/index.template.html
+++ b/scouting/www/index.template.html
@@ -11,6 +11,11 @@
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous"
/>
+ <link
+ rel="stylesheet"
+ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"
+ />
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<my-app></my-app>
diff --git a/scouting/www/view/BUILD b/scouting/www/view/BUILD
index fae4b23..b9b6030 100644
--- a/scouting/www/view/BUILD
+++ b/scouting/www/view/BUILD
@@ -17,8 +17,13 @@
visibility = ["//visibility:public"],
deps = [
"//scouting/webserver/requests/messages:error_response_ts_fbs",
- "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
- "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_driver_rankings_response_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_driver_rankings_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_notes_response_ts_fbs",
+ "//scouting/webserver/requests/messages:request_all_notes_ts_fbs",
+ "//scouting/webserver/requests/messages:request_data_scouting_response_ts_fbs",
+ "//scouting/webserver/requests/messages:request_data_scouting_ts_fbs",
+ "//scouting/www/rpc",
"@com_github_google_flatbuffers//ts:flatbuffers_ts",
"@npm//@angular/common",
"@npm//@angular/core",
diff --git a/scouting/www/view/view.component.ts b/scouting/www/view/view.component.ts
index 8150efd..30f5914 100644
--- a/scouting/www/view/view.component.ts
+++ b/scouting/www/view/view.component.ts
@@ -1,8 +1,189 @@
import {Component, OnInit} from '@angular/core';
+import {
+ Ranking,
+ RequestAllDriverRankingsResponse,
+} from 'org_frc971/scouting/webserver/requests/messages/request_all_driver_rankings_response_generated';
+import {
+ Stats,
+ RequestDataScoutingResponse,
+} from 'org_frc971/scouting/webserver/requests/messages/request_data_scouting_response_generated';
+import {
+ Note,
+ RequestAllNotesResponse,
+} from 'org_frc971/scouting/webserver/requests/messages/request_all_notes_response_generated';
+
+import {ViewDataRequestor} from '../rpc/view_data_requestor';
+
+type Source = 'Notes' | 'Stats' | 'DriverRanking';
+
+//TODO(Filip): Deduplicate
+const COMP_LEVEL_LABELS = {
+ qm: 'Qualifications',
+ ef: 'Eighth Finals',
+ qf: 'Quarter Finals',
+ sf: 'Semi Finals',
+ f: 'Finals',
+};
@Component({
selector: 'app-view',
templateUrl: './view.ng.html',
styleUrls: ['../common.css', './view.component.css'],
})
-export class ViewComponent {}
+export class ViewComponent {
+ constructor(private readonly viewDataRequestor: ViewDataRequestor) {}
+
+ // Make COMP_LEVEL_LABELS available in view.ng.html.
+ readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS;
+
+ // Progress and error messages to display to
+ // the user when fetching data.
+ progressMessage: string = '';
+ errorMessage: string = '';
+
+ // The current data source being displayed.
+ currentSource: Source = 'Notes';
+
+ // Current sort (ascending/descending match numbers).
+ // noteList is sorted based on team number until match
+ // number is added for note scouting.
+ ascendingSort = true;
+
+ // Stores the corresponding data.
+ noteList: Note[] = [];
+ driverRankingList: Ranking[] = [];
+ statList: Stats[] = [];
+
+ // Fetch notes on initialization.
+ ngOnInit() {
+ this.fetchCurrentSource();
+ }
+
+ // Called when a user changes the sort direction.
+ // Changes the data order between ascending/descending.
+ sortData() {
+ this.ascendingSort = !this.ascendingSort;
+ if (!this.ascendingSort) {
+ this.driverRankingList.sort((a, b) => b.matchNumber() - a.matchNumber());
+ this.noteList.sort((a, b) => b.team() - a.team());
+ this.statList.sort((a, b) => b.match() - a.match());
+ } else {
+ this.driverRankingList.sort((a, b) => a.matchNumber() - b.matchNumber());
+ this.noteList.sort((a, b) => a.team() - b.team());
+ this.statList.sort((a, b) => a.match() - b.match());
+ }
+ }
+
+ // Called when a user selects a new data source
+ // from the dropdown.
+ switchDataSource(target: Source) {
+ this.currentSource = target;
+ this.progressMessage = '';
+ this.errorMessage = '';
+ this.noteList = [];
+ this.driverRankingList = [];
+ this.statList = [];
+ this.fetchCurrentSource();
+ }
+
+ // Call the method to fetch data for the current source.
+ fetchCurrentSource() {
+ switch (this.currentSource) {
+ case 'Notes': {
+ this.fetchNotes();
+ }
+
+ case 'Stats': {
+ this.fetchStats();
+ }
+
+ case 'DriverRanking': {
+ this.fetchDriverRanking();
+ }
+ }
+ }
+
+ // TODO(Filip): Add delete functionality.
+ // Gets called when a user clicks the delete icon.
+ async deleteData() {
+ const block_alerts = document.getElementById(
+ 'block_alerts'
+ ) as HTMLInputElement;
+ if (!block_alerts.checked) {
+ if (!window.confirm('Actually delete data?')) {
+ this.errorMessage = 'Deleting data has not been implemented yet.';
+ return;
+ }
+ }
+ }
+
+ // Fetch all driver ranking data and store in driverRankingList.
+ async fetchDriverRanking() {
+ this.progressMessage = 'Fetching driver ranking data. Please be patient.';
+ this.errorMessage = '';
+
+ try {
+ this.driverRankingList =
+ await this.viewDataRequestor.fetchDriverRankingList();
+ this.progressMessage = 'Successfully fetched driver ranking data.';
+ } catch (e) {
+ this.errorMessage = e;
+ this.progressMessage = '';
+ }
+ }
+
+ // Fetch all data scouting (stats) data and store in statList.
+ async fetchStats() {
+ this.progressMessage = 'Fetching stats list. Please be patient.';
+ this.errorMessage = '';
+
+ try {
+ this.statList = await this.viewDataRequestor.fetchStatsList();
+ this.progressMessage = 'Successfully fetched stats list.';
+ } catch (e) {
+ this.errorMessage = e;
+ this.progressMessage = '';
+ }
+ }
+
+ // Fetch all notes data and store in noteList.
+ async fetchNotes() {
+ this.progressMessage = 'Fetching notes list. Please be patient.';
+ this.errorMessage = '';
+
+ try {
+ this.noteList = await this.viewDataRequestor.fetchNoteList();
+ this.progressMessage = 'Successfully fetched note list.';
+ } catch (e) {
+ this.errorMessage = e;
+ this.progressMessage = '';
+ }
+ }
+
+ // Parse all selected keywords for a note entry
+ // into one string to be displayed in the table.
+ parseKeywords(entry: Note) {
+ let parsedKeywords = '';
+
+ if (entry.goodDriving()) {
+ parsedKeywords += 'Good Driving ';
+ }
+ if (entry.badDriving()) {
+ parsedKeywords += 'Bad Driving ';
+ }
+ if (entry.solidClimb()) {
+ parsedKeywords += 'Solid Climb ';
+ }
+ if (entry.sketchyClimb()) {
+ parsedKeywords += 'Sketchy Climb ';
+ }
+ if (entry.goodDefense()) {
+ parsedKeywords += 'Good Defense ';
+ }
+ if (entry.badDefense()) {
+ parsedKeywords += 'Bad Defense ';
+ }
+
+ return parsedKeywords;
+ }
+}
diff --git a/scouting/www/view/view.ng.html b/scouting/www/view/view.ng.html
index b7ed627..c361638 100644
--- a/scouting/www/view/view.ng.html
+++ b/scouting/www/view/view.ng.html
@@ -1 +1,147 @@
<h2>View Data</h2>
+<!-- Drop down to select data type. -->
+<div class="dropdown">
+ <button
+ class="btn btn-secondary dropdown-toggle"
+ type="button"
+ data-bs-toggle="dropdown"
+ aria-expanded="false"
+ >
+ {{currentSource}}
+ </button>
+ <ul class="dropdown-menu">
+ <li>
+ <a class="dropdown-item" href="#" (click)="switchDataSource('Notes')">
+ Notes
+ </a>
+ </li>
+ <li>
+ <a class="dropdown-item" href="#" (click)="switchDataSource('Stats')">
+ Stats
+ </a>
+ </li>
+ <li>
+ <a
+ class="dropdown-item"
+ href="#"
+ (click)="switchDataSource('DriverRanking')"
+ >
+ Driver Ranking
+ </a>
+ </li>
+ </ul>
+</div>
+<h4>{{errorMessage}}</h4>
+<h4>{{progressMessage}}</h4>
+
+<ng-container [ngSwitch]="currentSource">
+ <!-- Notes Data Display. -->
+ <div *ngSwitchCase="'Notes'">
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col" class="d-flex flex-row">
+ <div class="align-self-center">Team</div>
+ <div class="align-self-center" *ngIf="ascendingSort">
+ <i (click)="sortData()" class="bi bi-caret-up"></i>
+ </div>
+ <div class="align-self-center" *ngIf="!ascendingSort">
+ <i (click)="sortData()" class="bi bi-caret-down"></i>
+ </div>
+ </th>
+ <th scope="col">Match</th>
+ <th scope="col">Note</th>
+ <th scope="col">Keywords</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let note of noteList; index as i;">
+ <th scope="row">{{note.team()}}</th>
+ <!-- Placeholder for match number. -->
+ <td>0</td>
+ <td>{{note.notes()}}</td>
+ <td>{{parseKeywords(note)}}</td>
+ <!-- Delete Icon. -->
+ <td>
+ <button class="btn btn-danger" (click)="deleteData()">
+ <i class="bi bi-trash"></i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!-- Stats Data Display. -->
+ <div *ngSwitchCase="'Stats'">
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col" class="d-flex flex-row">
+ <div class="align-self-center">Match</div>
+ <div class="align-self-center" *ngIf="ascendingSort">
+ <i (click)="sortData()" class="bi bi-caret-up"></i>
+ </div>
+ <div class="align-self-center" *ngIf="!ascendingSort">
+ <i (click)="sortData()" class="bi bi-caret-down"></i>
+ </div>
+ </th>
+ <th scope="col">Team</th>
+ <th scope="col">Set</th>
+ <th scope="col">Comp Level</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let stat of statList; index as i;">
+ <th scope="row">{{stat.match()}}</th>
+ <td>{{stat.team()}}</td>
+ <td>{{stat.setNumber()}}</td>
+ <td>{{COMP_LEVEL_LABELS[stat.compLevel()]}}</td>
+ <!-- Delete Icon. -->
+ <td>
+ <button class="btn btn-danger" (click)="deleteData()">
+ <i class="bi bi-trash"></i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <!-- Driver Ranking Data Display. -->
+ <div *ngSwitchCase="'DriverRanking'">
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col" class="d-flex flex-row">
+ <div class="align-self-center">Match</div>
+ <div class="align-self-center" *ngIf="ascendingSort">
+ <i (click)="sortData()" class="bi bi-caret-up"></i>
+ </div>
+ <div class="align-self-center" *ngIf="!ascendingSort">
+ <i (click)="sortData()" class="bi bi-caret-down"></i>
+ </div>
+ </th>
+ <th scope="col">Rank1</th>
+ <th scope="col">Rank2</th>
+ <th scope="col">Rank3</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr *ngFor="let ranking of driverRankingList; index as i;">
+ <th scope="row">{{ranking.matchNumber()}}</th>
+ <td>{{ranking.rank1()}}</td>
+ <td>{{ranking.rank2()}}</td>
+ <td>{{ranking.rank3()}}</td>
+ <!-- Delete Icon. -->
+ <td>
+ <button class="btn btn-danger" (click)="deleteData()">
+ <i class="bi bi-trash"></i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</ng-container>