diff --git a/modules/ui/src/app/components/stepper/stepper.component.html b/modules/ui/src/app/components/stepper/stepper.component.html new file mode 100644 index 000000000..e2a9d1adb --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.html @@ -0,0 +1,35 @@ +
+
+ +
+ +
+ +
+ + +
diff --git a/modules/ui/src/app/components/stepper/stepper.component.scss b/modules/ui/src/app/components/stepper/stepper.component.scss new file mode 100644 index 000000000..87c7ffecd --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.scss @@ -0,0 +1,83 @@ +/** + * Copyright 2023 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 + * + * https://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 '../../../theming/colors'; + +.form-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.form-header { + padding: 24px; +} + +.form-content { + display: grid; + padding: 24px; + gap: 10px; + overflow: auto; +} + +.form-footer { + margin-top: auto; + display: inline-block; + text-align: center; + padding-bottom: 24px; + width: 100%; +} + +.form-steps { + display: inline-flex; + text-align: center; + align-items: center; + height: 100%; +} + +.form-step { + border: 2px solid $lighter-grey; + width: 4px; + height: 4px; + display: inline-block; + border-radius: 50%; + margin: 0 8px; + &.step-active { + border-color: $secondary; + background: $secondary; + } +} + +.form-button-back { + float: left; +} + +.form-button-forward { + float: right; +} + +.form-button-back, +.form-button-forward { + & mat-icon { + color: $secondary; + width: 24px; + height: 24px; + font-size: 24px; + } + + &.hidden { + display: none; + } +} diff --git a/modules/ui/src/app/components/stepper/stepper.component.spec.ts b/modules/ui/src/app/components/stepper/stepper.component.spec.ts new file mode 100644 index 000000000..aa3fad17b --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StepperComponent } from './stepper.component'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-stepper-bypass', + template: + '' + + '
Header
', +}) +class TestStepperComponent {} + +describe('StepperComponent', () => { + let component: TestStepperComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StepperComponent], + declarations: [TestStepperComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestStepperComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have title', () => { + expect(fixture.nativeElement.querySelector('.form-header')).toBeTruthy(); + }); +}); diff --git a/modules/ui/src/app/components/stepper/stepper.component.ts b/modules/ui/src/app/components/stepper/stepper.component.ts new file mode 100644 index 000000000..ea1377c33 --- /dev/null +++ b/modules/ui/src/app/components/stepper/stepper.component.ts @@ -0,0 +1,36 @@ +import { Component, Input, TemplateRef } from '@angular/core'; +import { CdkStepper, CdkStepperModule } from '@angular/cdk/stepper'; +import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common'; +import { MatIcon } from '@angular/material/icon'; +import { MatButton, MatIconButton } from '@angular/material/button'; + +@Component({ + selector: 'app-stepper', + standalone: true, + imports: [ + NgForOf, + NgTemplateOutlet, + CdkStepperModule, + NgIf, + MatIcon, + MatIconButton, + MatButton, + ], + templateUrl: './stepper.component.html', + styleUrl: './stepper.component.scss', + providers: [{ provide: CdkStepper, useExisting: StepperComponent }], +}) +export class StepperComponent extends CdkStepper { + @Input() header: TemplateRef | undefined; + @Input() activeClass = 'active'; + + stepsCount = [1, 2, 3, 4]; //TODO will be removed when all steps are implemented + + forwardButtonHidden() { + return this.selectedIndex === this.stepsCount.length - 1; + } + + backButtonHidden() { + return this.selectedIndex === 0; + } +} diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html new file mode 100644 index 000000000..b0253ebc8 --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.html @@ -0,0 +1,137 @@ +
+ +
+ +

{{ data.title }}

+
+
+ + + + + Device Manufacturer + + Please enter device manufacturer name + + Please, check. The manufacturer name must be a maximum of 28 + characters. Only letters, numbers, and accented letters are + permitted. + + + Device Manufacturer is required + + + + Device Model + + Please enter device name + + Please, check. The device model name must be a maximum of 28 + characters. Only letters, numbers, and accented letters are + permitted. + + + Device Model is required + + + + MAC address + + Please enter MAC address + + MAC address is required + + + Please, check. A MAC address consists of 12 hexadecimal digits (0 + to 9, a to f, or A to F). + + + This MAC address is already used for another device in the + repository. + + + + Please, select the testing journey for device + + + + + + 🛡️ Device Qualification + + + + + + 🚀 Pilot Assessment + + + + + + + + + + Second step + + +
diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss new file mode 100644 index 000000000..cd5ce35b7 --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.scss @@ -0,0 +1,159 @@ +/** + * Copyright 2023 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 + * + * https://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 'src/theming/colors'; +@import 'src/theming/variables'; + +$form-max-width: 732px; +$form-min-width: 732px; +$form-height: 993px; + +:host { + display: grid; + grid-template-rows: 1fr; + overflow: auto; + grid-template-columns: minmax(285px, $form-max-width); +} + +.device-qualification-form { + overflow: hidden; +} + +.device-form { + display: grid; + padding: 24px; + max-width: $form-max-width; + min-width: $form-min-width; + + gap: 10px; + overflow: auto; +} + +.manufacturer-field, +.model-field { + &::ng-deep.mat-mdc-form-field-subscript-wrapper:has(mat-error) { + height: 40px; + } +} + +.device-form-title { + color: $grey-800; + font-size: 22px; + line-height: 28px; + padding-bottom: 14px; +} + +::ng-deep .device-form-test-modules { + overflow: auto; + min-height: 78px; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(4, 1fr); + grid-auto-flow: column; + padding-top: 16px; + p { + margin: 8px 0; + } +} + +.device-form-actions { + padding: 0; + min-height: 30px; +} + +.close-button { + color: $primary; +} + +.device-form-mac-address-error { + white-space: nowrap; +} + +.delete-button { + color: $primary; + margin-right: auto; +} + +.hidden { + display: none; +} + +.device-qualification-form-header { + position: relative; + padding-top: 24px; + &-title { + margin: 0; + font-size: 22px; + font-style: normal; + font-weight: 400; + line-height: 28px; + color: $dark-grey; + text-align: center; + padding: 38px 0; + background-image: url(/assets/icons/create_device_header.svg); + } + + &-close-button { + position: absolute; + right: 0; + top: 0; + min-width: 24px; + width: 24px; + height: 24px; + box-sizing: content-box; + line-height: normal !important; + padding: 0; + margin: 0; + + .close-button-icon { + width: 24px; + height: 24px; + margin: 0; + } + + ::ng-deep * { + line-height: inherit !important; + } + } +} + +.device-qualification-form-journey-label { + font-family: $font-secondary; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: $grey-800; + margin: 24px 16px 0 16px; +} + +.device-qualification-form-journey-button { + padding: 0 18px 0 24px; +} + +.device-qualification-form-journey-button-label { + font-family: $font-secondary; + font-style: normal; + font-weight: bold; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: $grey-800; +} + +.device-qualification-form-test-modules-container { + padding: 0 24px; +} diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts new file mode 100644 index 000000000..7269cc539 --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.spec.ts @@ -0,0 +1,318 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeviceQualificationFromComponent } from './device-qualification-from.component'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { MatButtonModule } from '@angular/material/button'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatInputModule } from '@angular/material/input'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; +import { device, MOCK_TEST_MODULES } from '../../../../mocks/device.mock'; +import { MatIconTestingModule } from '@angular/material/icon/testing'; +describe('DeviceQualificationFromComponent', () => { + let component: DeviceQualificationFromComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + DeviceQualificationFromComponent, + MatButtonModule, + ReactiveFormsModule, + MatCheckboxModule, + MatInputModule, + MatDialogModule, + BrowserAnimationsModule, + DeviceTestsComponent, + SpinnerComponent, + NgxMaskDirective, + NgxMaskPipe, + MatIconTestingModule, + ], + providers: [ + { + provide: MatDialogRef, + useValue: { + keydownEvents: () => of(new KeyboardEvent('keydown', { code: '' })), + close: () => ({}), + }, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + provideNgxMask(), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DeviceQualificationFromComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + component.data = { + testModules: MOCK_TEST_MODULES, + devices: [], + }; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain device form', () => { + const form = compiled.querySelector('.device-qualification-form'); + + expect(form).toBeTruthy(); + }); + + it('should close dialog on "cancel" click', () => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const closeButton = compiled.querySelector( + '.device-qualification-form-header-close-button' + ) as HTMLButtonElement; + + closeButton?.click(); + + expect(closeSpy).toHaveBeenCalledWith(); + + closeSpy.calls.reset(); + }); + + it('should not save data when fields are empty', () => { + const forwardButton = compiled.querySelector( + '.form-button-forward' + ) as HTMLButtonElement; + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + + ['', ' '].forEach(value => { + model.value = value; + model.dispatchEvent(new Event('input')); + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + forwardButton?.click(); + fixture.detectChanges(); + + const requiredErrors = compiled.querySelectorAll('mat-error'); + expect(requiredErrors?.length).toEqual(3); + + requiredErrors.forEach(error => { + expect(error?.innerHTML).toContain('required'); + }); + }); + }); + + describe('test modules', () => { + it('should be present', () => { + const test = compiled.querySelectorAll('mat-checkbox'); + + expect(test.length).toEqual(2); + }); + + it('should be enabled', () => { + const tests = compiled.querySelectorAll('.device-form-test-modules p'); + + expect(tests[0].classList.contains('disabled')).toEqual(false); + }); + }); + + describe('device model', () => { + it('should not contain errors when input is correct', () => { + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + ['model', 'Gebäude', 'jardín'].forEach(value => { + model.value = value; + model.dispatchEvent(new Event('input')); + + const errors = component.model.errors; + const uiValue = model.value; + const formValue = component.model.value; + + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); + }); + }); + + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value', + 'as&@3$', + ].forEach(value => { + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + model.value = value; + model.dispatchEvent(new Event('input')); + component.model.markAsTouched(); + fixture.detectChanges(); + + const modelError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.model.hasError('invalid_format'); + + expect(error).toBeTruthy(); + expect(modelError).toContain( + 'The device model name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' + ); + }); + }); + }); + + describe('device manufacturer', () => { + it('should not contain errors when input is correct', () => { + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + ['manufacturer', 'Gebäude', 'jardín'].forEach(value => { + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + + const errors = component.manufacturer.errors; + const uiValue = manufacturer.value; + const formValue = component.manufacturer.value; + + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); + }); + }); + + it('should have "invalid_format" error when field does not satisfy validation', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value', + 'as&@3$', + ].forEach(value => { + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + component.manufacturer.markAsTouched(); + fixture.detectChanges(); + + const manufacturerError = + compiled.querySelector('mat-error')?.innerHTML; + const error = component.manufacturer.hasError('invalid_format'); + + expect(error).toBeTruthy(); + expect(manufacturerError).toContain( + 'The manufacturer name must be a maximum of 28 characters. Only letters, numbers, and accented letters are permitted.' + ); + }); + }); + }); + + describe('mac address', () => { + it('should not be disabled', () => { + expect(component.mac_addr.disabled).toBeFalse(); + }); + + it('should not contain errors when input is correct', () => { + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + ['07:07:07:07:07:07', ' 07:07:07:07:07:07 '].forEach(value => { + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + + const errors = component.mac_addr.errors; + const formValue = component.mac_addr.value; + + expect(macAddress.value).toEqual(formValue); + expect(errors).toBeNull(); + }); + }); + + it('should have "pattern" error when field does not satisfy pattern', () => { + ['value', 'q01e423573c4'].forEach(value => { + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + component.mac_addr.markAsTouched(); + fixture.detectChanges(); + + const macAddressError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.mac_addr.hasError('pattern'); + + expect(error).toBeTruthy(); + expect(macAddressError).toContain( + 'Please, check. A MAC address consists of 12 hexadecimal digits (0 to 9, a to f, or A to F).' + ); + }); + }); + + it('should have "has_same_mac_address" error when MAC address is already used', () => { + component.data = { + testModules: MOCK_TEST_MODULES, + devices: [device], + }; + component.ngOnInit(); + fixture.detectChanges(); + + const macAddress: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-mac-address' + ) as HTMLInputElement; + macAddress.value = '00:1e:42:35:73:c4'; + macAddress.dispatchEvent(new Event('input')); + component.mac_addr.markAsTouched(); + fixture.detectChanges(); + + const macAddressError = compiled.querySelector('mat-error')?.innerHTML; + const error = component.mac_addr.hasError('has_same_mac_address'); + + expect(error).toBeTruthy(); + expect(macAddressError).toContain( + 'This MAC address is already used for another device in the repository.' + ); + }); + }); + + describe('when device is present', () => { + beforeEach(() => { + component.data = { + devices: [device], + testModules: MOCK_TEST_MODULES, + device: { + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + udmi: { + enabled: true, + }, + }, + }, + }; + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should fill form values with device values', () => { + const model: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-model' + ) as HTMLInputElement; + const manufacturer: HTMLInputElement = compiled.querySelector( + '.device-qualification-form-manufacturer' + ) as HTMLInputElement; + + expect(model.value).toEqual('O3-DIN-CPU'); + expect(manufacturer.value).toEqual('Delta'); + }); + }); +}); diff --git a/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts new file mode 100644 index 000000000..cd42d4b2a --- /dev/null +++ b/modules/ui/src/app/pages/devices/components/device-qualification-from/device-qualification-from.component.ts @@ -0,0 +1,184 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { + AbstractControl, + FormArray, + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { DeviceValidators } from '../device-form/device.validators'; +import { Device, TestModule } from '../../../../model/device'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; +import { CommonModule } from '@angular/common'; +import { CdkStep } from '@angular/cdk/stepper'; +import { StepperComponent } from '../../../../components/stepper/stepper.component'; +import { + MatError, + MatFormField, + MatFormFieldModule, +} from '@angular/material/form-field'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { MatButtonModule } from '@angular/material/button'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { TextFieldModule } from '@angular/cdk/text-field'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; +import { MatIcon } from '@angular/material/icon'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; +import { ProfileValidators } from '../../../risk-assessment/profile-form/profile.validators'; + +const MAC_ADDRESS_PATTERN = + '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; + +interface DialogData { + title?: string; + device?: Device; + devices: Device[]; + testModules: TestModule[]; +} + +@Component({ + selector: 'app-device-qualification-from', + standalone: true, + imports: [ + CdkStep, + StepperComponent, + MatFormField, + DeviceTestsComponent, + MatButtonModule, + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatError, + MatFormFieldModule, + MatSelectModule, + MatCheckboxModule, + TextFieldModule, + NgxMaskDirective, + NgxMaskPipe, + MatIcon, + MatRadioGroup, + MatRadioButton, + ], + providers: [provideNgxMask()], + templateUrl: './device-qualification-from.component.html', + styleUrl: './device-qualification-from.component.scss', +}) +export class DeviceQualificationFromComponent + extends EscapableDialogComponent + implements OnInit +{ + testModules: TestModule[] = []; + deviceQualificationForm!: FormGroup; + firstStep!: FormGroup; + device: Device | undefined; + + get model() { + return this.firstStep.get('model') as AbstractControl; + } + + get manufacturer() { + return this.firstStep.get('manufacturer') as AbstractControl; + } + + get mac_addr() { + return this.firstStep.get('mac_addr') as AbstractControl; + } + + get test_modules() { + return this.firstStep.controls['test_modules'] as FormArray; + } + + get first_step() { + return this.firstStep; + } + + // TODO dummy step to show next button; should be removed after next step is created + get second_step() { + return (this.deviceQualificationForm.get('steps') as FormArray).controls[ + '1' + ] as FormGroup; + } + + constructor( + private fb: FormBuilder, + private deviceValidators: DeviceValidators, + private profileValidators: ProfileValidators, + public override dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData + ) { + super(dialogRef); + this.device = data.device; + } + + ngOnInit(): void { + this.createDeviceForm(); + this.testModules = this.data.testModules; + if (this.data.device) { + this.model.setValue(this.data.device.model); + this.manufacturer.setValue(this.data.device.manufacturer); + this.mac_addr.setValue(this.data.device.mac_addr); + } + } + + submit(): void { + this.device = this.createDeviceFromForm(this.first_step); + } + + closeForm(): void { + this.dialogRef.close(); + } + + private createDeviceFromForm(formGroup: FormGroup): Device { + const testModules: { [key: string]: { enabled: boolean } } = {}; + formGroup.value.test_modules.forEach((enabled: boolean, i: number) => { + testModules[this.testModules[i]?.name] = { + enabled: enabled, + }; + }); + return { + model: this.model.value.trim(), + manufacturer: this.manufacturer.value.trim(), + mac_addr: this.mac_addr.value.trim(), + test_modules: testModules, + } as Device; + } + + private createDeviceForm() { + this.firstStep = this.fb.group({ + model: [ + '', + [ + this.profileValidators.textRequired(), + this.deviceValidators.deviceStringFormat(), + ], + ], + manufacturer: [ + '', + [ + this.profileValidators.textRequired(), + this.deviceValidators.deviceStringFormat(), + ], + ], + mac_addr: [ + '', + [ + this.profileValidators.textRequired(), + Validators.pattern(MAC_ADDRESS_PATTERN), + this.deviceValidators.differentMACAddress( + this.data.devices, + this.data.device + ), + ], + ], + test_modules: new FormArray([]), + testing_journey: [0], + }); + this.deviceQualificationForm = this.fb.group({ + steps: this.fb.array([this.firstStep, this.fb.group({})]), + }); + } +} diff --git a/modules/ui/src/app/pages/devices/devices.component.spec.ts b/modules/ui/src/app/pages/devices/devices.component.spec.ts index ba99b6a9a..26f7a569a 100644 --- a/modules/ui/src/app/pages/devices/devices.component.spec.ts +++ b/modules/ui/src/app/pages/devices/devices.component.spec.ts @@ -25,10 +25,7 @@ import { Device } from '../../model/device'; import { DevicesComponent } from './devices.component'; import { DevicesModule } from './devices.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { - DeviceFormComponent, - FormAction, -} from './components/device-form/device-form.component'; +import { FormAction } from './components/device-form/device-form.component'; import { MatDialogRef } from '@angular/material/dialog'; import { device, MOCK_TEST_MODULES } from '../../mocks/device.mock'; import { SimpleDialogComponent } from '../../components/simple-dialog/simple-dialog.component'; @@ -42,6 +39,7 @@ import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { Component } from '@angular/core'; import { MOCK_PROGRESS_DATA_IN_PROGRESS } from '../../mocks/testrun.mock'; +import { DeviceQualificationFromComponent } from './components/device-qualification-from/device-qualification-from.component'; describe('DevicesComponent', () => { let component: DevicesComponent; @@ -149,7 +147,7 @@ describe('DevicesComponent', () => { it('should open device dialog on "add device button" click', () => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), - } as MatDialogRef); + } as MatDialogRef); fixture.detectChanges(); const button = compiled.querySelector( '.device-add-button' @@ -158,11 +156,11 @@ describe('DevicesComponent', () => { expect(button).toBeTruthy(); expect(openSpy).toHaveBeenCalled(); - expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { - ariaLabel: 'Create device', + expect(openSpy).toHaveBeenCalledWith(DeviceQualificationFromComponent, { + ariaLabel: 'Create Device', data: { device: null, - title: 'Create device', + title: 'Create Device', testModules: [], devices: [device, device, device], }, @@ -179,13 +177,13 @@ describe('DevicesComponent', () => { it('should open device dialog on item click', () => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), - } as MatDialogRef); + } as MatDialogRef); fixture.detectChanges(); component.openDialog([device], MOCK_TEST_MODULES, device); expect(openSpy).toHaveBeenCalled(); - expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { + expect(openSpy).toHaveBeenCalledWith(DeviceQualificationFromComponent, { ariaLabel: 'Edit device', data: { device: device, @@ -205,12 +203,12 @@ describe('DevicesComponent', () => { it('should open device dialog with delete-button focus element', () => { const openSpy = spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(true), - } as MatDialogRef); + } as MatDialogRef); fixture.detectChanges(); component.openDialog([device], MOCK_TEST_MODULES, device, true); - expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { + expect(openSpy).toHaveBeenCalledWith(DeviceQualificationFromComponent, { ariaLabel: 'Edit device', data: { device: device, @@ -238,7 +236,7 @@ describe('DevicesComponent', () => { it('should call setIsOpenAddDevice if dialog closes with null', () => { spyOn(component.dialog, 'open').and.returnValue({ afterClosed: () => of(null), - } as MatDialogRef); + } as MatDialogRef); component.openDialog([], MOCK_TEST_MODULES); @@ -252,7 +250,7 @@ describe('DevicesComponent', () => { device, action: FormAction.Delete, }), - } as MatDialogRef); + } as MatDialogRef); component.openDialog([device], MOCK_TEST_MODULES, device); diff --git a/modules/ui/src/app/pages/devices/devices.component.ts b/modules/ui/src/app/pages/devices/devices.component.ts index be172aed3..e320663bf 100644 --- a/modules/ui/src/app/pages/devices/devices.component.ts +++ b/modules/ui/src/app/pages/devices/devices.component.ts @@ -23,7 +23,6 @@ import { import { MatDialog } from '@angular/material/dialog'; import { Device, DeviceView, TestModule } from '../../model/device'; import { - DeviceFormComponent, FormAction, FormResponse, } from './components/device-form/device-form.component'; @@ -36,6 +35,7 @@ import { Router } from '@angular/router'; import { timer } from 'rxjs/internal/observable/timer'; import { TestrunInitiateFormComponent } from '../testrun/components/testrun-initiate-form/testrun-initiate-form.component'; import { DevicesStore } from './devices.store'; +import { DeviceQualificationFromComponent } from './components/device-qualification-from/device-qualification-from.component'; @Component({ selector: 'app-device-repository', @@ -115,11 +115,11 @@ export class DevicesComponent implements OnInit, OnDestroy { selectedDevice?: Device, focusDeleteButton = false ): void { - const dialogRef = this.dialog.open(DeviceFormComponent, { - ariaLabel: selectedDevice ? 'Edit device' : 'Create device', + const dialogRef = this.dialog.open(DeviceQualificationFromComponent, { + ariaLabel: selectedDevice ? 'Edit device' : 'Create Device', data: { device: selectedDevice || null, - title: selectedDevice ? 'Edit device' : 'Create device', + title: selectedDevice ? 'Edit device' : 'Create Device', testModules: testModules, devices, }, diff --git a/modules/ui/src/assets/icons/create_device_header.svg b/modules/ui/src/assets/icons/create_device_header.svg new file mode 100644 index 000000000..fd10cfa8f --- /dev/null +++ b/modules/ui/src/assets/icons/create_device_header.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +