Add an "Import match list" tab to the scouting web page
This patch adds a new tab to the scouting web page to import the match
list. You have to point the server at a config file with `--tba_config`.
See its `--help` for more information.
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I606915a6cc5776fe183da41ab8b04b063f57dafd
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
index 94e0222..282e248 100644
--- a/scouting/webserver/main.go
+++ b/scouting/webserver/main.go
@@ -45,7 +45,7 @@
blueAllianceConfigPtr := flag.String("tba_config", "",
"The path to your The Blue Alliance JSON config. "+
"It needs an \"api_key\" field with your TBA API key. "+
- "Optionally, it can have a \"url\" field with the TBA API base URL.")
+ "Optionally, it can have a \"base_url\" field with the TBA API base URL.")
flag.Parse()
database, err := db.NewDatabase(*dbPathPtr)
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index 39246d8..8a32f89 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -22,6 +22,7 @@
visibility = ["//visibility:public"],
deps = [
"//scouting/www/entry",
+ "//scouting/www/import_match_list",
"@npm//@angular/animations",
"@npm//@angular/common",
"@npm//@angular/core",
@@ -78,3 +79,9 @@
],
deps = [":main_bundle_compiled"],
)
+
+filegroup(
+ name = "common_css",
+ srcs = ["common.css"],
+ visibility = ["//scouting/www:__subpackages__"],
+)
diff --git a/scouting/www/app.ng.html b/scouting/www/app.ng.html
index f763b7d..2fafceb 100644
--- a/scouting/www/app.ng.html
+++ b/scouting/www/app.ng.html
@@ -1,5 +1,13 @@
-<!--Progress Bar-->
-<!--<div class="row">
- <h1 class="text-end">Match {{matchNumber}}, Team {{teamNumber}}</h1>
-</div>-->
-<app-entry></app-entry>
+<ul class="nav nav-tabs">
+ <li class="nav-item">
+ <a class="nav-link" [class.active]="tabIs('Entry')" (click)="switchTabTo('Entry')">Data Entry</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [class.active]="tabIs('ImportMatchList')" (click)="switchTabTo('ImportMatchList')">Import Match List</a>
+ </li>
+</ul>
+
+<ng-container [ngSwitch]="tab">
+ <app-entry *ngSwitchCase="'Entry'"></app-entry>
+ <app-import-match-list *ngSwitchCase="'ImportMatchList'"></app-import-match-list>
+</ng-container>
diff --git a/scouting/www/app.ts b/scouting/www/app.ts
index f6247d3..285d306 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app.ts
@@ -1,8 +1,20 @@
import {Component} from '@angular/core';
+type Tab = 'Entry'|'ImportMatchList';
+
@Component({
selector: 'my-app',
templateUrl: './app.ng.html',
+ styleUrls: ['./common.css']
})
export class App {
+ tab: Tab = 'Entry';
+
+ tabIs(tab: Tab) {
+ return this.tab == tab;
+ }
+
+ switchTabTo(tab: Tab) {
+ this.tab = tab;
+ }
}
diff --git a/scouting/www/app_module.ts b/scouting/www/app_module.ts
index 6a6fff0..2a514f2 100644
--- a/scouting/www/app_module.ts
+++ b/scouting/www/app_module.ts
@@ -2,6 +2,7 @@
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {EntryModule} from './entry/entry.module';
+import {ImportMatchListModule} from './import_match_list/import_match_list.module';
import {App} from './app';
@@ -11,6 +12,7 @@
BrowserModule,
BrowserAnimationsModule,
EntryModule,
+ ImportMatchListModule,
],
exports: [App],
bootstrap: [App],
diff --git a/scouting/www/common.css b/scouting/www/common.css
new file mode 100644
index 0000000..35e943f
--- /dev/null
+++ b/scouting/www/common.css
@@ -0,0 +1,10 @@
+/* This CSS is shared between all scouting app tabs. */
+
+.error_message {
+ color: red;
+}
+.error_message:empty, .progress_message:empty {
+ /* TODO(phil): Figure out a way to make these take up no horizontal space.
+ * I.e. It would be nice to keep the error message and the progress message
+ * aligned when they are non-empty. */
+}
diff --git a/scouting/www/entry/BUILD b/scouting/www/entry/BUILD
index 55b6937..2c394de 100644
--- a/scouting/www/entry/BUILD
+++ b/scouting/www/entry/BUILD
@@ -2,13 +2,15 @@
ts_library(
name = "entry",
- srcs = glob([
- "*.ts",
- ]),
- angular_assets = glob([
- "*.ng.html",
- "*.css",
- ]),
+ srcs = [
+ "entry.component.ts",
+ "entry.module.ts",
+ ],
+ angular_assets = [
+ "entry.component.css",
+ "entry.ng.html",
+ "//scouting/www:common_css",
+ ],
compiler = "//tools:tsc_wrapped_with_angular",
target_compatible_with = ["@platforms//cpu:x86_64"],
use_angular_plugin = True,
diff --git a/scouting/www/entry/entry.component.css b/scouting/www/entry/entry.component.css
index 7cbc6d7..76a3c29 100644
--- a/scouting/www/entry/entry.component.css
+++ b/scouting/www/entry/entry.component.css
@@ -19,10 +19,6 @@
height: 150px;
}
-.error_message {
- color: red;
-}
-
button {
touch-action: manipulation;
}
diff --git a/scouting/www/entry/entry.component.ts b/scouting/www/entry/entry.component.ts
index ddc31df..fb4a1b1 100644
--- a/scouting/www/entry/entry.component.ts
+++ b/scouting/www/entry/entry.component.ts
@@ -15,7 +15,7 @@
@Component({
selector: 'app-entry',
templateUrl: './entry.ng.html',
- styleUrls: ['./entry.component.css']
+ styleUrls: ['../common.css', './entry.component.css']
})
export class EntryComponent {
section: Section = 'Team Selection';
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index c210ea3..b5783d2 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -209,7 +209,7 @@
<li>Defense Played Rating: {{defensePlayedScore}}</li>
</ul>
- <div class="error_message">{{ errorMessage }}</div>
+ <span class="error_message">{{ errorMessage }}</span>
<div class="buttons">
<button class="btn btn-primary" (click)="prevSection()">Back</button>
diff --git a/scouting/www/import_match_list/BUILD b/scouting/www/import_match_list/BUILD
new file mode 100644
index 0000000..9e40794
--- /dev/null
+++ b/scouting/www/import_match_list/BUILD
@@ -0,0 +1,27 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+ts_library(
+ name = "import_match_list",
+ srcs = [
+ "import_match_list.component.ts",
+ "import_match_list.module.ts",
+ ],
+ angular_assets = [
+ "import_match_list.component.css",
+ "import_match_list.ng.html",
+ "//scouting/www:common_css",
+ ],
+ compiler = "//tools:tsc_wrapped_with_angular",
+ target_compatible_with = ["@platforms//cpu:x86_64"],
+ use_angular_plugin = True,
+ visibility = ["//visibility:public"],
+ deps = [
+ "//scouting/webserver/requests/messages:error_response_ts_fbs",
+ "//scouting/webserver/requests/messages:refresh_match_list_response_ts_fbs",
+ "//scouting/webserver/requests/messages:refresh_match_list_ts_fbs",
+ "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+ "@npm//@angular/common",
+ "@npm//@angular/core",
+ "@npm//@angular/forms",
+ ],
+)
diff --git a/scouting/www/import_match_list/import_match_list.component.css b/scouting/www/import_match_list/import_match_list.component.css
new file mode 100644
index 0000000..0570c72
--- /dev/null
+++ b/scouting/www/import_match_list/import_match_list.component.css
@@ -0,0 +1,3 @@
+* {
+ padding: 10px;
+}
diff --git a/scouting/www/import_match_list/import_match_list.component.ts b/scouting/www/import_match_list/import_match_list.component.ts
new file mode 100644
index 0000000..b2b15e5
--- /dev/null
+++ b/scouting/www/import_match_list/import_match_list.component.ts
@@ -0,0 +1,53 @@
+import { Component, OnInit } from '@angular/core';
+
+import * as flatbuffer_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
+import * as error_response from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
+import * as refresh_match_list_response from 'org_frc971/scouting/webserver/requests/messages/refresh_match_list_response_generated';
+import * as refresh_match_list from 'org_frc971/scouting/webserver/requests/messages/refresh_match_list_generated';
+import RefreshMatchList = refresh_match_list.scouting.webserver.requests.RefreshMatchList;
+import RefreshMatchListResponse = refresh_match_list_response.scouting.webserver.requests.RefreshMatchListResponse;
+import ErrorResponse = error_response.scouting.webserver.requests.ErrorResponse;
+
+@Component({
+ selector: 'app-import-match-list',
+ templateUrl: './import_match_list.ng.html',
+ styleUrls: ['../common.css', './import_match_list.component.css']
+})
+export class ImportMatchListComponent {
+ year: number = new Date().getFullYear();
+ eventCode: string = '';
+ progressMessage: string = '';
+ errorMessage: string = '';
+
+ async importMatchList() {
+ this.errorMessage = '';
+
+ const builder = new flatbuffer_builder.Builder() as unknown as flatbuffers.Builder;
+ const eventCode = builder.createString(this.eventCode);
+ RefreshMatchList.startRefreshMatchList(builder);
+ RefreshMatchList.addYear(builder, this.year);
+ RefreshMatchList.addEventCode(builder, eventCode);
+ builder.finish(RefreshMatchList.endRefreshMatchList(builder));
+
+ this.progressMessage = 'Importing match list. Please be patient.';
+
+ const buffer = builder.asUint8Array();
+ const res = await fetch(
+ '/requests/refresh_match_list', {method: 'POST', body: buffer});
+
+ if (res.ok) {
+ // We successfully submitted the data.
+ this.progressMessage = 'Successfully imported match list.';
+ } else {
+ this.progressMessage = '';
+ const resBuffer = await res.arrayBuffer();
+ const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+ const parsedResponse = ErrorResponse.getRootAsErrorResponse(
+ fbBuffer as unknown as flatbuffers.ByteBuffer);
+
+ const errorMessage = parsedResponse.errorMessage();
+ this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+ }
+ }
+}
diff --git a/scouting/www/import_match_list/import_match_list.module.ts b/scouting/www/import_match_list/import_match_list.module.ts
new file mode 100644
index 0000000..1da8bec
--- /dev/null
+++ b/scouting/www/import_match_list/import_match_list.module.ts
@@ -0,0 +1,12 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {ImportMatchListComponent} from './import_match_list.component';
+import {FormsModule} from '@angular/forms';
+
+@NgModule({
+ declarations: [ImportMatchListComponent],
+ exports: [ImportMatchListComponent],
+ imports: [CommonModule, FormsModule],
+})
+export class ImportMatchListModule {
+}
diff --git a/scouting/www/import_match_list/import_match_list.ng.html b/scouting/www/import_match_list/import_match_list.ng.html
new file mode 100644
index 0000000..3c7ffa0
--- /dev/null
+++ b/scouting/www/import_match_list/import_match_list.ng.html
@@ -0,0 +1,20 @@
+<div class="header">
+ <h2>Import Match List</h2>
+</div>
+
+<div class="container-fluid">
+ <div class="row">
+ <label for="year">Year</label>
+ <input [(ngModel)]="year" type="number" id="year" min="1970" max="2500">
+ </div>
+ <div class="row">
+ <label for="event_code">Event Code</label>
+ <input [(ngModel)]="eventCode" type="text" id="event_code">
+ </div>
+
+ <span class="progress_message">{{ progressMessage }}</span>
+ <span class="error_message">{{ errorMessage }}</span>
+ <div class="text-right">
+ <button class="btn btn-primary" (click)="importMatchList()">Import</button>
+ </div>
+</div>