From cc0df1f61d9c05632ea29c0d741a4ff647007dd0 Mon Sep 17 00:00:00 2001 From: Luke Simonson Date: Tue, 22 Oct 2024 14:13:31 -0700 Subject: [PATCH] Add test control --- openhtf/output/web_gui/package-lock.json | 39 +++-- .../stations/station/station.component.html | 28 ++-- .../station/station.component.spec.ts | 2 + .../station/test-control.component.html | 45 ++++++ .../station/test-control.component.scss | 77 ++++++++++ .../station/test-control.component.ts | 142 ++++++++++++++++++ .../src/app/stations/stations.module.ts | 4 + openhtf/output/web_gui/src/style/base.scss | 6 +- openhtf/output/web_gui/src/style/main.scss | 2 +- 9 files changed, 313 insertions(+), 32 deletions(-) create mode 100644 openhtf/output/web_gui/src/app/stations/station/test-control.component.html create mode 100644 openhtf/output/web_gui/src/app/stations/station/test-control.component.scss create mode 100644 openhtf/output/web_gui/src/app/stations/station/test-control.component.ts diff --git a/openhtf/output/web_gui/package-lock.json b/openhtf/output/web_gui/package-lock.json index e1d5eb352..55318e606 100644 --- a/openhtf/output/web_gui/package-lock.json +++ b/openhtf/output/web_gui/package-lock.json @@ -1313,6 +1313,16 @@ "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", @@ -4357,9 +4367,9 @@ "dev": true, "optional": true, "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" } }, "fs.realpath": { @@ -4456,8 +4466,8 @@ "dev": true, "optional": true, "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" + "ajv": "^4.9.1", + "har-schema": "^1.0.5" } }, "has-unicode": { @@ -4523,7 +4533,7 @@ "dev": true, "optional": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "is-typedarray": { @@ -4618,7 +4628,7 @@ "dev": true, "optional": true, "requires": { - "mime-db": "1.27.0" + "mime-db": "~1.27.0" } }, "minimatch": { @@ -4627,7 +4637,7 @@ "dev": true, "optional": true, "requires": { - "brace-expansion": "1.1.7" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -13894,7 +13904,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -13919,6 +13933,13 @@ } } }, + "nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "dev": true, + "optional": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/openhtf/output/web_gui/src/app/stations/station/station.component.html b/openhtf/output/web_gui/src/app/stations/station/station.component.html index 465dc58b3..f5d138b09 100644 --- a/openhtf/output/web_gui/src/app/stations/station/station.component.html +++ b/openhtf/output/web_gui/src/app/stations/station/station.component.html @@ -17,18 +17,11 @@
@@ -52,19 +45,18 @@
+ +
-
+ +
Displaying test record for a previous test run
 ({{ selectedTest.startTimeMillis | timeAgo }})
-
@@ -77,9 +69,7 @@
-
diff --git a/openhtf/output/web_gui/src/app/stations/station/station.component.spec.ts b/openhtf/output/web_gui/src/app/stations/station/station.component.spec.ts index 1bb937611..203e6d83d 100644 --- a/openhtf/output/web_gui/src/app/stations/station/station.component.spec.ts +++ b/openhtf/output/web_gui/src/app/stations/station/station.component.spec.ts @@ -28,6 +28,7 @@ import {TimeAgoPipe} from '../../shared/time-ago.pipe'; import {StationComponent} from './station.component'; import {StationService} from './station.service'; +import {TestControlComponent} from './test-control.component'; // Selectors for components used in the station component template which take // a single test state as input. @@ -123,6 +124,7 @@ describe('station component', () => { TestBed.configureTestingModule({ declarations: (testWidgetStubs as Array<{}>).concat([ HistoryComponentStub, + TestControlComponent, StationComponent, HostComponent, TimeAgoPipe, diff --git a/openhtf/output/web_gui/src/app/stations/station/test-control.component.html b/openhtf/output/web_gui/src/app/stations/station/test-control.component.html new file mode 100644 index 000000000..a46756fb8 --- /dev/null +++ b/openhtf/output/web_gui/src/app/stations/station/test-control.component.html @@ -0,0 +1,45 @@ +
+
+ +
+
Start new test
+
+ +
+ + +
+ {{ error }} +
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
diff --git a/openhtf/output/web_gui/src/app/stations/station/test-control.component.scss b/openhtf/output/web_gui/src/app/stations/station/test-control.component.scss new file mode 100644 index 000000000..4f97d5966 --- /dev/null +++ b/openhtf/output/web_gui/src/app/stations/station/test-control.component.scss @@ -0,0 +1,77 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import 'vars'; + +// Styling for lists injected into the prompt. +:host ::ng-deep ol, +:host ::ng-deep ul { + padding-left: 25px; + margin: 0; +} + +.test-control-has-error { + border-color: $theme-red; + + &:focus { + border-color: darken($theme-red, 20%); + } +} + +.test-control-error-text { + color: $theme-red; + font-size: $font-size-small; +} + +.dropdown { + position: relative; +} + +ul { + list-style: none; + padding: 0; + margin: 0; + width: 100%; + max-height: 300px; + overflow-y: auto; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + position: absolute; + background-color: #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +li { + padding: 10px; + cursor: pointer; + transition: background-color 0.3s; +} + +li.selected { + background-color: #3498db; + color: #fff; +} + +li:hover { + background-color: #f2f2f2; + cursor: pointer; +} + +li:hover.selected { + background-color: #3498db; + color: #fff; +} diff --git a/openhtf/output/web_gui/src/app/stations/station/test-control.component.ts b/openhtf/output/web_gui/src/app/stations/station/test-control.component.ts new file mode 100644 index 000000000..038c749f9 --- /dev/null +++ b/openhtf/output/web_gui/src/app/stations/station/test-control.component.ts @@ -0,0 +1,142 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Component representing the UserInput plug. + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { trigger } from '@angular/animations'; +import { Headers, Http, RequestOptions, Response } from '@angular/http'; + + +import { ConfigService } from '../../core/config.service'; +import { FlashMessageService } from '../../core/flash-message.service'; +import { Station } from '../../shared/models/station.model'; +import { TestState, TestStatus } from '../../shared/models/test-state.model'; +import { washIn } from '../../shared/animations'; +import { getStationBaseUrl, messageFromErrorResponse } from '../../shared/util'; + +export class TestSelectedEvent { + constructor(public test: TestState) {} +} + +@Component({ + animations: [trigger('animateIn', washIn)], + selector: 'htf-test-control', + templateUrl: './test-control.component.html', + styleUrls: ['./test-control.component.scss'], +}) +export class TestControlComponent implements OnInit { + @Input() test: TestState; + @Input() station: Station; + @Output() onSelectTest = new EventEmitter(); + + options: RequestOptions + stationBaseUrl: string + + tests: string[] = []; + filteredTests: string[] = []; + searchText = ''; + selectedValue = ''; + hoveredItem: string | null = null; + isDropdownOpen = false; + + constructor( + protected config: ConfigService, + protected http: Http, protected flashMessage: FlashMessageService) { + let headers = new Headers({'Content-Type': 'application/json'}); + this.options = new RequestOptions({headers}); + this.stationBaseUrl = getStationBaseUrl(this.config.dashboardEnabled, this.station); + } + + testRunning(): boolean { + return this.test && this.test.status === TestStatus.running; + } + + hasTests(): boolean { + return this.tests.length !== 0; + } + + ngOnInit() { + this.getTests(); + } + + filterItems() { + if (this.searchText === '') { + this.filteredTests = this.tests.slice(); + } else { + this.filteredTests = this.tests.filter(item => item.toLowerCase().includes(this.searchText.toLowerCase())); + } + } + + selectItem(item: string) { + this.searchText = item; + this.selectedValue = item; + this.isDropdownOpen = false; + } + + onMouseEnter(item: string) { + this.hoveredItem = item; + } + + onMouseLeave() { + this.hoveredItem = null; + } + + protected getTests() { + const testsUrl = `${this.stationBaseUrl}/list_tests`; + + this.http.get(testsUrl, this.options).subscribe((resp: Response) => { + this.tests = resp.json().tests; + this.filteredTests = this.tests.slice(); + }); + } + + protected startTest(test_name: string) { + const testUrl = `${this.stationBaseUrl}/tests/${test_name}`; + const payload = JSON.stringify({'method': 'remote_execute', 'kwargs': {}}); + + this.http.post(testUrl, payload, this.options) + .subscribe(() => {}, (error: Response) => { + const tooltip = messageFromErrorResponse(error); + this.flashMessage.error( + `An error occurred trying to start test ${test_name}`, + tooltip); + }); + } + + abort() { + const abortUrl = `${this.stationBaseUrl}/abort`; + + this.http.post(abortUrl, this.options) + .subscribe(() => {}, (error: Response) => { + const tooltip = messageFromErrorResponse(error); + this.flashMessage.error( + `An error occurred trying to abort current test}`, + tooltip); + }); + } + + sendTestStart(input: HTMLInputElement) { + this.onSelectTest.emit(new TestSelectedEvent(null)); + let response: string; + response = input.value; + input.value = ''; + this.startTest(response); + } + +} diff --git a/openhtf/output/web_gui/src/app/stations/stations.module.ts b/openhtf/output/web_gui/src/app/stations/stations.module.ts index 1a5a419a1..e4f463041 100644 --- a/openhtf/output/web_gui/src/app/stations/stations.module.ts +++ b/openhtf/output/web_gui/src/app/stations/stations.module.ts @@ -23,6 +23,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { PlugsModule } from '../plugs/plugs.module'; import { SharedModule } from '../shared/shared.module'; @@ -38,12 +39,14 @@ import { PhaseComponent } from './station/phase.component'; import { StationComponent } from './station/station.component'; import { StationService } from './station/station.service'; import { TestSummaryComponent } from './station/test-summary.component'; +import { TestControlComponent } from './station/test-control.component'; @NgModule({ imports: [ CommonModule, PlugsModule, SharedModule, + FormsModule, ], declarations: [ StationListComponent, @@ -54,6 +57,7 @@ import { TestSummaryComponent } from './station/test-summary.component'; PhaseListComponent, StationComponent, TestSummaryComponent, + TestControlComponent, ], providers: [ DashboardService, diff --git a/openhtf/output/web_gui/src/style/base.scss b/openhtf/output/web_gui/src/style/base.scss index 35ce59b24..2c3ea5f5f 100644 --- a/openhtf/output/web_gui/src/style/base.scss +++ b/openhtf/output/web_gui/src/style/base.scss @@ -66,16 +66,16 @@ input[type='text'] { @keyframes htf-base-input-pulse { 0% { box-shadow: 0 0 0 0 rgba($theme-blue, .25), - 0 0 10px rgba($shadow-black, .1) inset; + 0 0 10px rgba($shadow-black, .1) inset; } 70% { box-shadow: 0 0 0 8px rgba($theme-blue, 0), - 0 0 10px rgba($shadow-black, .1) inset; + 0 0 10px rgba($shadow-black, .1) inset; } 100% { box-shadow: 0 0 0 0 rgba($theme-blue, 0), - 0 0 10px rgba($shadow-black, .1) inset; + 0 0 10px rgba($shadow-black, .1) inset; } } diff --git a/openhtf/output/web_gui/src/style/main.scss b/openhtf/output/web_gui/src/style/main.scss index 834ebd6b7..a5370c130 100644 --- a/openhtf/output/web_gui/src/style/main.scss +++ b/openhtf/output/web_gui/src/style/main.scss @@ -17,5 +17,5 @@ @import 'base'; @import 'components'; @import 'layout'; -@import 'normalize'; // From node-normalize-scss. +@import 'normalize'; // From node-normalize-scss. @import 'util';