blob: 4f18f77c3fa193df5367cf19d5ec4730e36304e6 [file] [log] [blame]
/**
* Copyright 2019 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 {LiveAnnouncer} from '@angular/cdk/a11y';
import {COMMA, ENTER} from '@angular/cdk/keycodes';
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {MatButton} from '@angular/material/button';
import {MatChipInputEvent} from '@angular/material/chips';
import {MatStepper} from '@angular/material/stepper';
import {ActivatedRoute, Params, Router} from '@angular/router';
import {forkJoin, of as observableOf, ReplaySubject, Subscription} from 'rxjs';
import {first} from 'rxjs/operators';
import {TestResourceClassType, TestResourceForm} from '../build_channels/test_resource_form';
import {MttClient} from '../services/mtt_client';
import * as mttModels from '../services/mtt_models';
import {Notifier} from '../services/notifier';
import {FormChangeTracker} from '../shared/can_deactivate';
import {buildApiErrorMessage, resetStepCompletion} from '../shared/util';
enum Step {
CONFIGURE_TEST_PLAN = 0,
CONFIGURE_TEST_RUN = 1,
ADD_DEVICE_ACTIONS = 2,
SET_TEST_RESOURCES = 3
}
const TOTAL_STEPS = 4;
/**
* Form for creating or editing a test plan
*/
@Component({
selector: 'test-plan-edit-page',
styleUrls: ['test_plan_edit_page.css'],
templateUrl: './test_plan_edit_page.ng.html',
})
export class TestPlanEditPage extends FormChangeTracker implements
OnDestroy, OnInit, AfterViewInit {
@ViewChild('backButton', {static: false}) backButton?: MatButton;
// Validation variables
@ViewChild(TestResourceForm, {static: true})
testResourceForm!: TestResourceForm;
errorMessage = '';
// Record each step whether it has finished or not
stepCompletionStatusMap: {[stepNum: number]: boolean} = {};
step = Step;
totalSteps = TOTAL_STEPS;
data: Partial<mttModels.TestPlan> = mttModels.initTestPlan();
selectedDeviceActions: mttModels.DeviceAction[] = [];
selectedTestRunActions: mttModels.TestRunAction[] = [];
editMode = false;
nodeConfigTestResourceUrls: mttModels.NameValuePair[] = [];
testResourceClassType = TestResourceClassType;
testResourceObjs: mttModels.TestResourceObj[] = [];
/** Keys used to separate labels */
readonly separatorKeyCodes: number[] = [ENTER, COMMA];
isLoading = false;
deviceInfoSubscription?: Subscription;
private readonly destroy = new ReplaySubject<void>(1);
constructor(
private readonly liveAnnouncer: LiveAnnouncer,
private readonly mttClient: MttClient,
private readonly notifier: Notifier,
private readonly route: ActivatedRoute,
private readonly router: Router,
) {
super();
}
ngOnInit() {
this.route.params.pipe(first()).subscribe((params: Params) => {
this.loadData(params['id']);
});
// Initialize step completion status map
resetStepCompletion(0, this.stepCompletionStatusMap, this.totalSteps);
}
ngOnDestroy() {
this.destroy.next();
this.destroy.complete();
}
/**
* Triggered on click next button in stepper
* @param stepper MatStepper
*/
goForward(stepper: MatStepper): void {
if (this.validateStep(stepper.selectedIndex)) {
this.stepCompletionStatusMap[stepper.selectedIndex] = true;
// wait for data to populate stepper
setTimeout(() => {
stepper.next();
}, 100);
}
}
/**
* Validate each step and populate error messages
* @param currentStep Indicate which step are we validating
*/
validateStep(currentStep: Step): boolean {
this.errorMessage = '';
this.invalidInputs = [];
switch (currentStep) {
case Step.CONFIGURE_TEST_PLAN: {
this.invalidInputs = this.getInvalidInputs();
return !this.invalidInputs.length;
}
case Step.CONFIGURE_TEST_RUN: {
const res = !!this.data.test_run_sequences &&
!!this.data.test_run_sequences.length;
if (!res) {
this.errorMessage = 'Test run configuration is required';
}
return res;
}
default: {
break;
}
}
return true;
}
onStepperSelectionChange(selectedIndex: number) {
resetStepCompletion(
selectedIndex, this.stepCompletionStatusMap, this.totalSteps);
}
ngAfterViewInit() {
this.backButton!.focus();
}
loadData(testPlanId: string|undefined) {
this.editMode = !!testPlanId;
if (this.editMode) {
this.isLoading = true;
this.liveAnnouncer.announce('Loading', 'polite');
}
const nodeConfigObs = this.mttClient.getNodeConfig();
const testPlanObs = testPlanId ? this.mttClient.getTestPlan(testPlanId) :
observableOf(null);
// Get API call results
forkJoin({
nodeConfig: nodeConfigObs,
testPlan: testPlanObs,
})
.pipe(first())
.subscribe(
(res) => {
// Node Configs
this.nodeConfigTestResourceUrls =
res.nodeConfig.test_resource_default_download_urls || [];
// Test Plan
this.loadTestPlan(res.testPlan);
if (this.editMode) {
this.isLoading = false;
this.liveAnnouncer.announce('Test plan loaded', 'assertive');
}
},
(error) => {
this.notifier.showError(
'Failed to load test plan.', buildApiErrorMessage(error));
});
}
loadTestPlan(testPlan: mttModels.TestPlan|null) {
if (!testPlan) {
return;
}
this.data = testPlan;
this.data.cron_exp = testPlan.cron_exp || '';
this.data.before_device_action_ids =
testPlan.before_device_action_ids || [];
this.data.labels = testPlan.labels || [];
this.data.test_resource_pipes = testPlan.test_resource_pipes || [];
}
addLabel(event: MatChipInputEvent) {
const input = event.chipInput.inputElement;
const value = event.value;
if ((value || '').trim() &&
this.data.labels!.indexOf(value.trim()) === -1) {
this.data.labels!.push(value.trim());
}
if (input) {
input.value = '';
}
}
removeLabel(label: string) {
const index = this.data.labels!.indexOf(label);
if (index >= 0) {
this.data.labels!.splice(index, 1);
}
}
updateConfigSequenceList(sequences: mttModels.TestRunSequence[]) {
this.data.test_run_sequences = sequences;
}
back() {
this.router.navigate(['test_plans']);
}
// create or update test plan
submit() {
if (!this.validateStep(Step.CONFIGURE_TEST_RUN)) {
return;
}
const resultTest: mttModels.TestPlan = {
id: this.data.id,
name: this.data.name!.trim(),
labels: this.data.labels,
cron_exp: this.data.cron_exp!.trim(),
cron_exp_timezone: this.data.cron_exp_timezone,
test_run_configs: [],
test_run_sequences: this.data.test_run_sequences,
};
if (this.editMode) {
this.mttClient.updateTestPlan(resultTest.id!, resultTest)
.subscribe(
() => {
super.resetForm();
this.back();
this.notifier.showMessage(
`Test plan '${resultTest.name}' updated`);
},
(error) => {
this.notifier.showError(
'Failed to update test plan.', buildApiErrorMessage(error));
});
} else {
this.mttClient.createTestPlan(resultTest)
.subscribe(
() => {
super.resetForm();
this.back();
this.notifier.showMessage(
`Test plan '${resultTest.name}' created`);
},
(error) => {
this.notifier.showError(
'Failed to create test plan.', buildApiErrorMessage(error));
});
}
}
}