diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py index 559117aec..7a7c179af 100644 --- a/framework/python/src/common/risk_profile.py +++ b/framework/python/src/common/risk_profile.py @@ -69,8 +69,7 @@ def __init__(self, profile_json=None, profile_format=None): 'r', encoding='utf-8') as device_format_file: device_format_json = json.load(device_format_file) - for step in device_format_json: - self._device_format.extend(step['questions']) + self._device_format = device_format_json except (IOError, ValueError) as e: LOGGER.error( 'An error occurred whilst loading the device profile format') diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.scss b/modules/ui/src/app/components/device-tests/device-tests.component.scss index a70c57d1b..d0ea57461 100644 --- a/modules/ui/src/app/components/device-tests/device-tests.component.scss +++ b/modules/ui/src/app/components/device-tests/device-tests.component.scss @@ -35,16 +35,16 @@ margin: 20px 0 8px; font-size: 18px; line-height: 24px; - color: colors.$grey-800; + color: colors.$on-surface-variant; } .device-tests-description { margin: 0; - font-family: variables.$font-secondary; + font-family: variables.$font-text; font-size: 14px; line-height: 20px; letter-spacing: 0.2px; - color: colors.$grey-800; + color: colors.$on-surface-variant; } .device-form-test-modules { diff --git a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss index 96c6bfc1b..3ef463e0d 100644 --- a/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss +++ b/modules/ui/src/app/components/dynamic-form/dynamic-form.component.scss @@ -19,12 +19,14 @@ @use 'variables'; .field-label { - margin: 0; - color: colors.$grey-800; - font-size: 18px; + font-family: variables.$font-text; + font-style: normal; + font-weight: 500; + font-size: 16px; line-height: 24px; - padding-top: 24px; - padding-bottom: 16px; + letter-spacing: 0.1px; + color: colors.$on-surface-variant; + padding: 20px 20px 8px 16px; display: inline-block; &:has(+ .field-select-multiple.ng-invalid.ng-dirty) { color: mat.get-theme-color($light-theme, error, 40); diff --git a/modules/ui/src/app/mocks/device.mock.ts b/modules/ui/src/app/mocks/device.mock.ts index 86ef4ffd7..19732c4c3 100644 --- a/modules/ui/src/app/mocks/device.mock.ts +++ b/modules/ui/src/app/mocks/device.mock.ts @@ -13,13 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Device, - DeviceStatus, - DeviceQuestionnaireSection, -} from '../model/device'; +import { Device, DeviceStatus } from '../model/device'; import { ProfileRisk } from '../model/profile'; -import { FormControlType } from '../model/question'; +import { FormControlType, QuestionFormat } from '../model/question'; export const device = { status: DeviceStatus.VALID, @@ -71,55 +67,45 @@ export const MOCK_TEST_MODULES = [ export const MOCK_MODULES = ['Connection', 'Udmi']; -export const DEVICES_FORM: DeviceQuestionnaireSection[] = [ +export const DEVICES_FORM: QuestionFormat[] = [ { - step: 1, - title: 'Step 1 title', - description: 'Step 1 description', - questions: [ + question: 'What type of device is this?', + type: FormControlType.SELECT, + options: [ { + text: 'Building Automation Gateway', + risk: ProfileRisk.HIGH, id: 1, - question: 'What type of device is this?', - type: FormControlType.SELECT, - options: [ - { - text: 'Building Automation Gateway', - risk: ProfileRisk.HIGH, - id: 1, - }, - { - text: 'IoT Gateway', - risk: ProfileRisk.LIMITED, - id: 2, - }, - ], }, { + text: 'IoT Gateway', + risk: ProfileRisk.LIMITED, id: 2, - question: 'Does your device process any sensitive information? ', - type: FormControlType.SELECT, - options: [ - { - id: 1, - text: 'Yes', - risk: ProfileRisk.LIMITED, - }, - { - id: 2, - text: 'No', - risk: ProfileRisk.HIGH, - }, - ], + }, + ], + }, + { + question: 'Does your device process any sensitive information? ', + type: FormControlType.SELECT, + options: [ + { + id: 1, + text: 'Yes', + risk: ProfileRisk.LIMITED, }, { - id: 3, - question: 'Please select the technology this device falls into', - type: FormControlType.SELECT, - options: [ - { text: 'Hardware - Access Control' }, - { text: 'Hardware - Air quality' }, - ], + id: 2, + text: 'No', + risk: ProfileRisk.HIGH, }, ], }, + { + question: 'Please select the technology this device falls into', + type: FormControlType.SELECT, + options: [ + { text: 'Hardware - Access Control' }, + { text: 'Hardware - Air quality' }, + ], + }, ]; diff --git a/modules/ui/src/app/model/device.ts b/modules/ui/src/app/model/device.ts index 5f73d3c28..62100f170 100644 --- a/modules/ui/src/app/model/device.ts +++ b/modules/ui/src/app/model/device.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { QuestionFormat } from './question'; import { Question } from './profile'; export interface Device { @@ -57,17 +56,6 @@ export enum DeviceView { WithActions = 'with actions', } -export interface DeviceQuestionnaireSection { - step: number; - title?: string; - description?: string; - questions: QuestionnaireFormat[]; -} - -export interface QuestionnaireFormat extends QuestionFormat { - id: number; -} - export enum TestingType { Pilot = 'Pilot Assessment', Qualification = 'Device Qualification', 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 index 087a3e74e..5d93022da 100644 --- 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 @@ -14,333 +14,138 @@ limitations under the License. -->
- -
- -

{{ data.title }}

-
-
- - - -

- {{ data.title }} dialogue step 1 -

-
- - 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 + + + + + Please, check. The manufacturer name must be a maximum of 28 + characters. Only letters, numbers, and accented letters are + permitted. - - - - - - Device Qualification - - - - - Pilot Assessment - - - - - - - At least one test has to be selected to save a Device. - -
-
- - - -

- {{ data.title }} dialogue step {{ step.step + 1 }} -

-
-

- {{ step.title }} -

-

- {{ step.description }} -

- -
-
-
+ + + Device Manufacturer is required + + + + + + + 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 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. + + - -

- {{ data.title }} dialogue last step -

-

- {{ data.title }} dialogue step 4 -

-
-
-

Summary

-

- - The device has been configured. Please check the setup. - - - No changes were made to the device configuration. - - The device cannot be configured -

-
-
- -
-

- Device type - {{ device?.type }} -

-

- Technology - {{ device?.technology }} -

-
-
-
- Select Save to create your new device. You will then be able to - carry on your device testing journey: -
    -
  • - Run Testrun against your device until you achieve a compliant - result -
  • -
  • Export the Testrun report and output files
  • -
  • Send the testing results to the lab for validation
  • -
-
+ Please, select the testing journey for device + -
-
-

- - error - - Unable to create the device -

-
-
-

- Validation error! -

-

- Please go back and correct the errors on - - , - - - and - - Step {{ step + 1 }}. -

+ + + + Device Qualification + + + + + Pilot Assessment + + + + + + + At least one test has to be selected to save a Device. + -

- All existing fields must be filled in. -

-
-
-
-
-
- - - -
-
-
-
+ +
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 index 1790bf6b5..be86387a8 100644 --- 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 @@ -31,25 +31,27 @@ $form-min-width: 285px; container-name: qualification-form; display: grid; - grid-template-rows: 1fr; - overflow: auto; - grid-template-columns: minmax($form-min-width, $form-max-width); - height: 100vh; - max-height: 978px; + height: 100%; + background: colors.$surface; + border-radius: 8px; + box-shadow: + 0px 4px 8px 3px rgba(60, 64, 67, 0.15), + 0px 1px 3px 0px rgba(60, 64, 67, 0.3); } .device-qualification-form { - overflow: hidden; + overflow: scroll; } ::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-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); grid-auto-flow: column; padding-top: 16px; + padding-left: 10px; p { margin: 8px 0; } @@ -99,14 +101,14 @@ $form-min-width: 285px; } .device-qualification-form-journey-label { - font-family: variables.$font-secondary; + font-family: variables.$font-text; font-style: normal; - font-weight: 400; + font-weight: 500; font-size: 16px; line-height: 24px; letter-spacing: 0.1px; - color: colors.$grey-800; - margin: 24px 16px 0 16px; + color: colors.$on-surface-variant; + padding: 20px 20px 8px 16px; } .device-qualification-form-journey-button { @@ -118,17 +120,12 @@ $form-min-width: 285px; } .device-qualification-form-journey-button-label { - font-family: variables.$font-secondary; + font-family: variables.$font-text; font-style: normal; - font-weight: 500; - font-size: 14px; - line-height: 20px; + font-weight: 400; + font-size: 16px; + line-height: 24px; letter-spacing: 0.2px; - color: colors.$grey-800; -} - -.device-qualification-form-test-modules-container { - padding: 0 24px; } .device-qualification-form-step-title { @@ -158,22 +155,10 @@ $form-min-width: 285px; cursor: pointer; } -.device-qualification-form-step-content { - padding: 0 16px; - overflow: scroll; -} - .device-qualification-form-page { - padding-top: 10px; - margin-top: -10px; display: grid; - gap: 8px; - height: 100%; - overflow: hidden; align-content: start; - &:has(.device-qualification-form-summary-container) { - grid-template-rows: min-content min-content 1fr min-content; - } + padding: 24px 16px; } .device-qualification-form-summary-container { @@ -310,6 +295,20 @@ $form-min-width: 285px; } } +::ng-deep .device-tests-description { + padding: 0 20px; +} + +::ng-deep .device-tests-title { + font-family: variables.$font-text; + font-style: normal; + font-weight: 500; + font-size: 16px !important; + line-height: 24px !important; + letter-spacing: 0.1px; + padding: 20px 20px 8px 16px; +} + .device-qualification-form-test-modules-container-error ::ng-deep .device-tests-title { 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 index 2087a64ea..6e9f83148 100644 --- 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 @@ -17,7 +17,6 @@ import { ComponentFixture, discardPeriodicTasks, fakeAsync, - flush, TestBed, tick, } from '@angular/core/testing'; @@ -31,7 +30,7 @@ import { import { of } from 'rxjs'; import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; import { MatButtonModule } from '@angular/material/button'; -import { FormArray, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatInputModule } from '@angular/material/input'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -46,7 +45,6 @@ import { MatIconTestingModule } from '@angular/material/icon/testing'; import { TestRunService } from '../../../../services/test-run.service'; import { DevicesStore } from '../../devices.store'; import { provideMockStore } from '@ngrx/store/testing'; -import { FormAction } from '../../devices.component'; import { DeviceStatus, TestingType } from '../../../../model/device'; import { Component, Input } from '@angular/core'; import { QuestionFormat } from '../../../../model/question'; @@ -68,9 +66,9 @@ describe('DeviceQualificationFromComponent', () => { const MOCK_DEVICE = { status: DeviceStatus.VALID, - manufacturer: '', - model: '', - mac_addr: '', + manufacturer: 'manufacturer', + model: 'model', + mac_addr: '01:01:01:01:01:01', test_pack: TestingType.Qualification, type: '', technology: '', @@ -134,12 +132,10 @@ describe('DeviceQualificationFromComponent', () => { component = fixture.componentInstance; compiled = fixture.nativeElement as HTMLElement; - component.data = { - testModules: MOCK_TEST_MODULES, - devices: [], - index: 0, - isCreate: true, - }; + fixture.componentRef.setInput('testModules', MOCK_TEST_MODULES); + fixture.componentRef.setInput('devices', []); + fixture.componentRef.setInput('isCreate', true); + testrunServiceMock.fetchQuestionnaireFormat.and.returnValue( of(DEVICES_FORM) ); @@ -160,180 +156,15 @@ describe('DeviceQualificationFromComponent', () => { }); it('should fetch devices format', () => { - fixture.detectChanges(); const getQuestionnaireFormatSpy = spyOn( component.devicesStore, 'getQuestionnaireFormat' ); - component.ngOnInit(); fixture.detectChanges(); expect(getQuestionnaireFormatSpy).toHaveBeenCalled(); }); - it('should close dialog on "cancel" click with do data if form has no changes', () => { - fixture.detectChanges(); - 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 close dialog on escape', fakeAsync(() => { - const closeSpy = spyOn(component.dialogRef, 'close'); - fixture.detectChanges(); - - keyboardEvent.next(new KeyboardEvent('keydown', { code: 'Escape' })); - - tick(); - - expect(closeSpy).toHaveBeenCalledWith(); - - closeSpy.calls.reset(); - })); - - it('should close dialog on submit with "Save" action', fakeAsync(() => { - component.device = MOCK_DEVICE; - const closeSpy = spyOn(component.dialogRef, 'close'); - fixture.detectChanges(); - - component.submit(); - tick(); - flush(); - - expect(closeSpy).toHaveBeenCalledWith({ - action: 'Save', - device: MOCK_DEVICE, - }); - - closeSpy.calls.reset(); - })); - - it('should close dialog on delete with "Delete" action', fakeAsync(() => { - const closeSpy = spyOn(component.dialogRef, 'close'); - fixture.detectChanges(); - - component.delete(); - tick(); - - expect(closeSpy).toHaveBeenCalledWith({ - action: 'Delete', - device: MOCK_DEVICE, - index: 0, - }); - - closeSpy.calls.reset(); - })); - - describe('#deviceHasNoChanges', () => { - const deviceProps = [ - { manufacturer: 'test' }, - { model: 'test' }, - { mac_addr: 'test' }, - { test_pack: TestingType.Pilot }, - { type: 'test' }, - { technology: 'test' }, - { - test_modules: { - udmi: { - enabled: false, - }, - }, - }, - { additional_info: undefined }, - { - additional_info: [ - { question: 'What type of device is this?', answer: 'test' }, - ], - }, - ]; - it('should return true if devices the same', () => { - const result = component.deviceHasNoChanges(MOCK_DEVICE, MOCK_DEVICE); - - expect(result).toBeTrue(); - }); - - deviceProps.forEach(item => { - it(`should return false if devices have different props`, () => { - const MOCK_DEVICE_1 = { ...MOCK_DEVICE, ...item }; - const result = component.deviceHasNoChanges(MOCK_DEVICE_1, MOCK_DEVICE); - - expect(result).toBeFalse(); - }); - }); - }); - - it('should trigger onResize method when window is resized ', () => { - fixture.detectChanges(); - const spyOnResize = spyOn(component, 'onResize'); - window.dispatchEvent(new Event('resize')); - fixture.detectChanges(); - expect(spyOnResize).toHaveBeenCalled(); - }); - - it('#goToStep should set selected index', () => { - fixture.detectChanges(); - component.goToStep(0); - - expect(component.stepper().selectedIndex).toBe(0); - }); - - it('should close dialog on "cancel" click', () => { - fixture.detectChanges(); - component.manufacturer.setValue('test'); - ( - component.deviceQualificationForm.get('steps') as FormArray - ).controls.forEach(control => control.markAsDirty()); - fixture.detectChanges(); - const closeSpy = spyOn(component.dialogRef, 'close'); - const closeButton = compiled.querySelector( - '.device-qualification-form-header-close-button' - ) as HTMLButtonElement; - - closeButton?.click(); - - expect(closeSpy).toHaveBeenCalledWith({ - action: FormAction.Close, - index: 0, - device: { - status: DeviceStatus.VALID, - manufacturer: 'test', - model: '', - mac_addr: '', - test_pack: 'Device Qualification', - type: '', - technology: '', - test_modules: { - udmi: { - enabled: true, - }, - connection: { - enabled: true, - }, - }, - additional_info: [ - { question: 'What type of device is this?', answer: '' }, - { - question: 'Does your device process any sensitive information? ', - answer: '', - }, - { - question: 'Please select the technology this device falls into', - answer: '', - }, - ], - }, - }); - - closeSpy.calls.reset(); - }); - describe('test modules', () => { beforeEach(() => { fixture.detectChanges(); @@ -463,15 +294,13 @@ describe('DeviceQualificationFromComponent', () => { }); describe('mac address', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - it('should not be disabled', () => { + fixture.detectChanges(); expect(component.mac_addr.disabled).toBeFalse(); }); it('should not contain errors when input is correct', () => { + fixture.detectChanges(); const macAddress: HTMLInputElement = compiled.querySelector( '.device-qualification-form-mac-address' ) as HTMLInputElement; @@ -488,6 +317,7 @@ describe('DeviceQualificationFromComponent', () => { }); it('should have "pattern" error when field does not satisfy pattern', () => { + fixture.detectChanges(); ['value', 'q01e423573c4'].forEach(value => { const macAddress: HTMLInputElement = compiled.querySelector( '.device-qualification-form-mac-address' @@ -508,13 +338,10 @@ describe('DeviceQualificationFromComponent', () => { }); it('should have "has_same_mac_address" error when MAC address is already used', () => { - component.data = { - testModules: MOCK_TEST_MODULES, - devices: [device], - index: 0, - isCreate: true, - }; - component.ngOnInit(); + fixture.componentRef.setInput('testModules', MOCK_TEST_MODULES); + fixture.componentRef.setInput('devices', [device]); + fixture.componentRef.setInput('isCreate', true); + fixture.detectChanges(); const macAddress: HTMLInputElement = compiled.querySelector( @@ -537,23 +364,20 @@ describe('DeviceQualificationFromComponent', () => { describe('when device is present', () => { beforeEach(() => { - component.data = { - devices: [device], - testModules: MOCK_TEST_MODULES, - device: { - status: DeviceStatus.VALID, - manufacturer: 'Delta', - model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', - test_modules: { - udmi: { - enabled: true, - }, + fixture.componentRef.setInput('testModules', MOCK_TEST_MODULES); + fixture.componentRef.setInput('devices', [device]); + fixture.componentRef.setInput('isCreate', false); + fixture.componentRef.setInput('initialDevice', { + status: DeviceStatus.VALID, + manufacturer: 'Delta', + model: 'O3-DIN-CPU', + mac_addr: '00:1e:42:35:73:c4', + test_modules: { + udmi: { + enabled: true, }, }, - isCreate: false, - index: 0, - }; + }); }); it('should fill form values with device values', fakeAsync(() => { @@ -579,297 +403,17 @@ describe('DeviceQualificationFromComponent', () => { })); }); - describe('steps', () => { - beforeEach(() => { + describe('onSaveClicked', () => { + it('should emit device', () => { fixture.detectChanges(); - }); - - describe('with questionnaire', () => { - it('should have steps', () => { - expect( - (component.deviceQualificationForm.get('steps') as FormArray).controls - .length - ).toEqual(3); - }); - }); - - 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('happy flow', () => { - beforeEach(() => { - component.model.setValue('model'); - component.manufacturer.setValue('manufacturer'); - component.mac_addr.setValue('07:07:07:07:07:07'); - component.test_modules.setValue([true, true]); - }); - - it('should save device when step is changed', () => { - const forwardButton = compiled.querySelector( - '.form-button-forward' - ) as HTMLButtonElement; - forwardButton.click(); - - expect(component.device).toEqual({ - status: DeviceStatus.VALID, - manufacturer: 'manufacturer', - model: 'model', - mac_addr: '07:07:07:07:07:07', - test_pack: TestingType.Qualification, - type: '', - technology: '', - test_modules: { - udmi: { - enabled: true, - }, - connection: { - enabled: true, - }, - }, - additional_info: [ - { question: 'What type of device is this?', answer: '' }, - { - question: 'Does your device process any sensitive information? ', - answer: '', - }, - { - question: 'Please select the technology this device falls into', - answer: '', - }, - ], - }); - }); - - describe('summary', () => { - beforeEach(() => { - const forwardButton = compiled.querySelector( - '.form-button-forward' - ) as HTMLButtonElement; - forwardButton.click(); // will redirect to 2 step - fixture.detectChanges(); - - const nextForwardButton = compiled.querySelector( - '.form-button-forward' - ) as HTMLButtonElement; - nextForwardButton.click(); //will redirect to summary - - fixture.detectChanges(); - }); - - it('should have device item', () => { - const item = compiled.querySelector('app-device-item'); - expect(item).toBeTruthy(); - }); - - it('should have instructions', () => { - const instructions = compiled.querySelector( - '.device-qualification-form-instructions' - ); - expect(instructions).toBeTruthy(); - }); - - it('should not have instructions when device is editing', () => { - component.data = { - devices: [device], - testModules: MOCK_TEST_MODULES, - device: { - status: DeviceStatus.VALID, - manufacturer: 'Delta', - model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', - test_modules: { - udmi: { - enabled: true, - }, - }, - }, - isCreate: false, - index: 0, - }; - fixture.detectChanges(); - - const instructions = compiled.querySelector( - '.device-qualification-form-instructions' - ); - expect(instructions).toBeNull(); - }); - - it('should save device', () => { - const saveSpy = spyOn(component.devicesStore, 'saveDevice'); - - component.submit(); - - const args = saveSpy.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].device).toEqual({ - status: DeviceStatus.VALID, - manufacturer: 'manufacturer', - model: 'model', - mac_addr: '07:07:07:07:07:07', - test_pack: 'Device Qualification', - type: '', - technology: '', - test_modules: { - connection: { - enabled: true, - }, - udmi: { - enabled: true, - }, - }, - additional_info: [ - { question: 'What type of device is this?', answer: '' }, - { - question: - 'Does your device process any sensitive information? ', - answer: '', - }, - { - question: 'Please select the technology this device falls into', - answer: '', - }, - ], - }); - expect(saveSpy).toHaveBeenCalled(); - }); - - it('should edit device', () => { - component.data = { - devices: [device], - testModules: MOCK_TEST_MODULES, - device: { - status: DeviceStatus.VALID, - manufacturer: 'Delta', - model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', - test_modules: { - udmi: { - enabled: true, - }, - }, - }, - isCreate: false, - index: 0, - }; - fixture.detectChanges(); - const editSpy = spyOn(component.devicesStore, 'editDevice'); - - component.submit(); - - const args = editSpy.calls.argsFor(0); - // @ts-expect-error config is in object - expect(args[0].device).toEqual({ - status: DeviceStatus.VALID, - manufacturer: 'manufacturer', - model: 'model', - mac_addr: '07:07:07:07:07:07', - test_pack: 'Device Qualification', - type: '', - technology: '', - test_modules: { - connection: { - enabled: true, - }, - udmi: { - enabled: true, - }, - }, - additional_info: [ - { question: 'What type of device is this?', answer: '' }, - { - question: - 'Does your device process any sensitive information? ', - answer: '', - }, - { - question: 'Please select the technology this device falls into', - answer: '', - }, - ], - }); - expect(editSpy).toHaveBeenCalled(); - }); - }); - }); - - describe('with errors', () => { - beforeEach(() => { - component.data = { - devices: [device], - testModules: MOCK_TEST_MODULES, - device: { - status: DeviceStatus.VALID, - manufacturer: 'Delta', - model: 'O3-DIN-CPU', - mac_addr: '00:1e:42:35:73:c4', - test_modules: { - udmi: { - enabled: true, - }, - }, - }, - isCreate: false, - index: 0, - }; - component.model.setValue(''); - - fixture.detectChanges(); - }); - - describe('summary', () => { - beforeEach(() => { - const forwardButton = compiled.querySelector( - '.form-button-forward' - ) as HTMLButtonElement; - forwardButton.click(); // will redirect to 2 step - fixture.detectChanges(); - - const nextForwardButton = compiled.querySelector( - '.form-button-forward' - ) as HTMLButtonElement; - nextForwardButton.click(); //will redirect to summary - fixture.detectChanges(); - }); - - it('should have error message', () => { - const error = compiled.querySelector( - '.device-qualification-form-summary-info-description' - ); - expect(error?.textContent?.trim()).toEqual( - 'Please go back and correct the errors on Step 1.' - ); - }); - }); + const saveSpy = spyOn(component.save, 'emit'); + component.manufacturer.setValue('manufacturer'); + component.model.setValue('model'); + component.mac_addr.setValue('01:01:01:01:01:01'); + component.deviceQualificationForm.markAsDirty(); + + component.onSaveClicked(); + expect(saveSpy).toHaveBeenCalledWith(MOCK_DEVICE); }); }); }); 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 index 409bd982d..4cb4e15f7 100644 --- 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 @@ -14,14 +14,12 @@ * limitations under the License. */ import { - AfterViewInit, Component, - ElementRef, - HostListener, OnDestroy, OnInit, - viewChild, inject, + input, + output, } from '@angular/core'; import { AbstractControl, @@ -34,16 +32,12 @@ import { import { DeviceValidators } from '../device-form/device.validators'; import { Device, - DeviceQuestionnaireSection, DeviceStatus, DeviceView, TestingType, TestModule, } from '../../../../model/device'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CommonModule } from '@angular/common'; -import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper'; -import { StepperComponent } from '../../../../components/stepper/stepper.component'; import { MatError, MatFormField, @@ -56,39 +50,21 @@ import { MatSelectModule } from '@angular/material/select'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { TextFieldModule } from '@angular/cdk/text-field'; import { NgxMaskDirective, 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'; import { DevicesStore } from '../../devices.store'; import { DynamicFormComponent } from '../../../../components/dynamic-form/dynamic-form.component'; -import { filter, skip, Subject, takeUntil, timer } from 'rxjs'; -import { FormAction, FormResponse } from '../../devices.component'; -import { DeviceItemComponent } from '../../../../components/device-item/device-item.component'; -import { ProgramTypeIconComponent } from '../../../../components/program-type-icon/program-type-icon.component'; +import { skip, Subject, takeUntil, timer } from 'rxjs'; import { Question } from '../../../../model/profile'; -import { FormControlType } from '../../../../model/question'; -import { ProgramType } from '../../../../model/program-type'; -import { FocusManagerService } from '../../../../services/focus-manager.service'; +import { FormControlType, QuestionFormat } from '../../../../model/question'; const MAC_ADDRESS_PATTERN = '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; -interface DialogData { - title?: string; - device?: Device; - initialDevice?: Device; - devices: Device[]; - testModules: TestModule[]; - index: number; - isCreate: boolean; -} - @Component({ selector: 'app-device-qualification-from', imports: [ - CdkStep, - StepperComponent, MatFormField, DeviceTestsComponent, MatButtonModule, @@ -101,251 +77,143 @@ interface DialogData { MatCheckboxModule, TextFieldModule, NgxMaskDirective, - MatIcon, MatRadioGroup, MatRadioButton, DynamicFormComponent, - DeviceItemComponent, - ProgramTypeIconComponent, ], providers: [provideNgxMask(), DevicesStore], templateUrl: './device-qualification-from.component.html', styleUrl: './device-qualification-from.component.scss', }) -export class DeviceQualificationFromComponent - implements OnInit, AfterViewInit, OnDestroy -{ +export class DeviceQualificationFromComponent implements OnInit, OnDestroy { + readonly TestingType = TestingType; + readonly DeviceView = DeviceView; + private fb = inject(FormBuilder); private deviceValidators = inject(DeviceValidators); private profileValidators = inject(ProfileValidators); - dialogRef = - inject>(MatDialogRef); - data = inject(MAT_DIALOG_DATA); + private destroy$: Subject = new Subject(); devicesStore = inject(DevicesStore); - private element = inject(ElementRef); - private focusService = inject(FocusManagerService); - readonly FORM_HEIGHT = 993; - readonly TestingType = TestingType; - readonly DeviceView = DeviceView; - readonly ProgramType = ProgramType; - readonly stepper = viewChild.required('stepper'); - testModules: TestModule[] = []; deviceQualificationForm: FormGroup = this.fb.group({}); - device: Device | undefined; - format: DeviceQuestionnaireSection[] = []; - selectedIndex: number = 0; - typeStep = 1; + format: QuestionFormat[] = []; typeQuestion = 0; - technologyStep = 1; technologyQuestion = 2; - private destroy$: Subject = new Subject(); + initialDevice = input(); + devices = input([]); + testModules = input([]); + isCreate = input(true); + + save = output(); + delete = output(); + cancel = output(); get model() { - return this.getStep(0).get('model') as AbstractControl; + return this.deviceQualificationForm.get('model') as AbstractControl; } get manufacturer() { - return this.getStep(0).get('manufacturer') as AbstractControl; + return this.deviceQualificationForm.get('manufacturer') as AbstractControl; } get mac_addr() { - return this.getStep(0).get('mac_addr') as AbstractControl; + return this.deviceQualificationForm.get('mac_addr') as AbstractControl; } get test_pack() { - return this.getStep(0).get('test_pack') as AbstractControl; + return this.deviceQualificationForm.get('test_pack') as AbstractControl; } get type() { - return this.getStep(this.typeStep)?.get( + return this.deviceQualificationForm.get( this.typeQuestion.toString() ) as AbstractControl; } get technology() { - return this.getStep(this.technologyStep)?.get( + return this.deviceQualificationForm.get( this.technologyQuestion.toString() ) as AbstractControl; } get test_modules() { - return this.getStep(0).controls['test_modules'] as FormArray; - } - - get formValid() { - return ( - this.deviceQualificationForm.get('steps') as FormArray - ).controls.every(control => (control as FormGroup).valid); - } - - deviceHasNoChanges(device1: Device | undefined, device2: Device | undefined) { - return device1 && device2 && this.compareDevices(device1, device2); - } - - @HostListener('window:resize', ['$event']) - onResize() { - this.setDialogHeight(); - } - - constructor() { - const data = this.data; - - this.device = data.device; + return this.deviceQualificationForm.controls['test_modules'] as FormArray; } ngOnInit(): void { - this.createBasicStep(); - this.testModules = this.data.testModules; + this.createDeviceForm(); this.devicesStore.questionnaireFormat$.pipe(skip(1)).subscribe(format => { - this.createDeviceForm(format); this.format = format; - format.forEach(step => { - step.questions.forEach((question, index) => { - // need to define the step and index of type and technology - if (question.question.toLowerCase().includes('type')) { - this.typeStep = step.step; - this.typeQuestion = index; - } else if (question.question.toLowerCase().includes('technology')) { - this.technologyStep = step.step; - this.technologyQuestion = index; - } - }); + format.forEach((question, index) => { + // need to define the step and index of type and technology + if (question.question.toLowerCase().includes('type')) { + this.typeQuestion = index; + } else if (question.question.toLowerCase().includes('technology')) { + this.technologyQuestion = index; + } }); timer(0) .pipe(takeUntil(this.destroy$)) .subscribe(() => { - if (this.data.device) { - this.fillDeviceForm(this.format, this.data.device!); - } - if (this.data.index) { - // previous steps should be marked as interacted - for (let i = 0; i <= this.data.index; i++) { - this.goToStep(i); - } + if (this.initialDevice()) { + this.fillDeviceForm(this.format, this.initialDevice()!); } - this.dialogRef - .keydownEvents() - .pipe(filter((e: KeyboardEvent) => e.code === 'Escape')) - .subscribe(() => { - this.closeForm(); - }); }); }); this.devicesStore.getQuestionnaireFormat(); } - - ngAfterViewInit() { - //set static height for better UX - this.element.nativeElement.style.height = - this.element.nativeElement.offsetHeight + 'px'; - } - ngOnDestroy() { this.destroy$.next(true); this.destroy$.unsubscribe(); } - submit(): void { - this.updateDevice(this.device!, () => { - this.dialogRef.close({ - action: FormAction.Save, - device: this.device, - } as FormResponse); - }); + onSaveClicked(): void { + this.save.emit(this.createDeviceFromForm()); } - delete(): void { - this.dialogRef.close({ - action: FormAction.Delete, - device: this.createDeviceFromForm(), - index: this.stepper().selectedIndex, - } as FormResponse); + onCancelClicked(): void { + this.cancel.emit(); } - closeForm(): void { - const device1 = this.data.initialDevice; - const device2 = this.createDeviceFromForm(); - if ( - (device1 && device2 && this.compareDevices(device1, device2)) || - (!device1 && this.deviceIsEmpty(device2)) - ) { - this.dialogRef.close(); - } else { - this.dialogRef.close({ - action: FormAction.Close, - device: this.createDeviceFromForm(), - index: this.stepper().selectedIndex, - } as FormResponse); - } - } - - getStep(step: number) { - return (this.deviceQualificationForm.get('steps') as FormArray).controls[ - step - ] as FormGroup; - } - - onStepChange(event: StepperSelectionEvent) { - this.focusService.focusFirstElementInContainer(); - if (event.previouslySelectedStep.completed) { - this.device = this.createDeviceFromForm(); - } + onDeleteClick(): void { + this.delete.emit(this.initialDevice()!); } - getErrorSteps(): number[] { - const steps: number[] = []; - (this.deviceQualificationForm.get('steps') as FormArray).controls.forEach( - (control, index) => { - if (!control.valid) steps.push(index); - } - ); - return steps; - } - - goToStep(index: number, event?: Event) { - event?.preventDefault(); - this.stepper().selectedIndex = index; - } - - private fillDeviceForm( - format: DeviceQuestionnaireSection[], - device: Device - ): void { - format.forEach(step => { - step.questions.forEach((question, index) => { - const answer = device.additional_info?.find( - answers => answers.question === question.question - )?.answer; - if (answer !== undefined && answer !== null && answer !== '') { - if (question.type === FormControlType.SELECT_MULTIPLE) { - question.options?.forEach((item, idx) => { - if ((answer as number[])?.includes(idx)) { - ( - this.getStep(step.step).get(index.toString()) as FormGroup - ).controls[idx].setValue(true); - } else { - ( - this.getStep(step.step).get(index.toString()) as FormGroup - ).controls[idx].setValue(false); - } - }); - } else { - ( - this.getStep(step.step).get(index.toString()) as AbstractControl - ).setValue(answer || ''); - } + private fillDeviceForm(format: QuestionFormat[], device: Device): void { + format.forEach((question, index) => { + const answer = device.additional_info?.find( + answers => answers.question === question.question + )?.answer; + if (answer !== undefined && answer !== null && answer !== '') { + if (question.type === FormControlType.SELECT_MULTIPLE) { + question.options?.forEach((item, idx) => { + if ((answer as number[])?.includes(idx)) { + ( + this.deviceQualificationForm.get(index.toString()) as FormGroup + ).controls[idx].setValue(true); + } else { + ( + this.deviceQualificationForm.get(index.toString()) as FormGroup + ).controls[idx].setValue(false); + } + }); } else { ( - this.getStep(step.step)?.get(index.toString()) as AbstractControl - )?.markAsTouched(); + this.deviceQualificationForm.get( + index.toString() + ) as AbstractControl + ).setValue(answer || ''); } - }); + } else { + ( + this.deviceQualificationForm.get(index.toString()) as AbstractControl + )?.markAsTouched(); + } }); this.model.setValue(device.model); this.manufacturer.setValue(device.manufacturer); @@ -365,23 +233,11 @@ export class DeviceQualificationFromComponent this.technology?.setValue(device.technology); } - private updateDevice(device: Device, callback: () => void) { - if (!this.data.isCreate && this.data.device) { - this.devicesStore.editDevice({ - device, - mac_addr: this.data.device.mac_addr, - onSuccess: callback, - }); - } else { - this.devicesStore.saveDevice({ device, onSuccess: callback }); - } - } - private createDeviceFromForm(): Device { const testModules: { [key: string]: { enabled: boolean } } = {}; - this.getStep(0).value.test_modules.forEach( + this.deviceQualificationForm.value.test_modules.forEach( (enabled: boolean, i: number) => { - testModules[this.testModules[i]?.name] = { + testModules[this.testModules()[i]?.name] = { enabled: enabled, }; } @@ -389,25 +245,23 @@ export class DeviceQualificationFromComponent const additionalInfo: Question[] = []; - this.format.forEach(step => { - step.questions.forEach((question, index) => { - const response: Question = {}; - response.question = question.question; + this.format.forEach((question, index) => { + const response: Question = {}; + response.question = question.question; - if (question.type === FormControlType.SELECT_MULTIPLE) { - const answer: number[] = []; - question.options?.forEach((_, idx) => { - const value = this.getStep(step.step).value[index][idx]; - if (value) { - answer.push(idx); - } - }); - response.answer = answer; - } else { - response.answer = this.getStep(step.step).value[index]?.trim(); - } - additionalInfo.push(response); - }); + if (question.type === FormControlType.SELECT_MULTIPLE) { + const answer: number[] = []; + question.options?.forEach((_, idx) => { + const value = this.deviceQualificationForm.value[index][idx]; + if (value) { + answer.push(idx); + } + }); + response.answer = answer; + } else { + response.answer = this.deviceQualificationForm.value[index]?.trim(); + } + additionalInfo.push(response); }); return { @@ -423,8 +277,8 @@ export class DeviceQualificationFromComponent } as Device; } - private createBasicStep() { - const firstStep = this.fb.group({ + private createDeviceForm() { + this.deviceQualificationForm = this.fb.group({ model: [ '', [ @@ -445,8 +299,8 @@ export class DeviceQualificationFromComponent this.profileValidators.textRequired(), Validators.pattern(MAC_ADDRESS_PATTERN), this.deviceValidators.differentMACAddress( - this.data.devices, - this.data.device + this.devices(), + this.initialDevice() ), ], ], @@ -456,121 +310,5 @@ export class DeviceQualificationFromComponent ), test_pack: [TestingType.Qualification], }); - - this.deviceQualificationForm = this.fb.group({ - steps: this.fb.array([firstStep]), - }); - } - - private createDeviceForm(format: DeviceQuestionnaireSection[]) { - format.forEach(() => { - (this.deviceQualificationForm.get('steps') as FormArray).controls.push( - this.createStep() - ); - }); - - // summary step - (this.deviceQualificationForm.get('steps') as FormArray).controls.push( - this.fb.group({}) - ); - } - - private createStep() { - return new FormGroup({}); - } - - private compareDevices(device1: Device, device2: Device) { - if (device1.manufacturer !== device2.manufacturer) { - return false; - } - if (device1.model !== device2.model) { - return false; - } - if (device1.mac_addr !== device2.mac_addr) { - return false; - } - if (device1.type !== device2.type) { - return false; - } - if (device1.technology !== device2.technology) { - return false; - } - if (device1.test_pack !== device2.test_pack) { - return false; - } - const keys1 = Object.keys(device1.test_modules!); - - for (const key of keys1) { - const val1 = device1.test_modules![key]; - const val2 = device2.test_modules![key]; - if (val1?.enabled !== val2?.enabled) { - return false; - } - } - - if (device1.additional_info) { - for (const question of device1.additional_info) { - if ( - question.answer !== - device2.additional_info?.find( - question2 => question2.question === question.question - )?.answer - ) { - return false; - } - } - } else { - return false; - } - return true; - } - - private deviceIsEmpty(device: Device) { - if (device.manufacturer !== '') { - return false; - } - if (device.model !== '') { - return false; - } - if (device.mac_addr !== '') { - return false; - } - if (device.type !== '') { - return false; - } - if (device.technology !== '') { - return false; - } - if (device.test_pack !== TestingType.Qualification) { - return false; - } - const keys1 = Object.keys(device.test_modules!); - - for (const key of keys1) { - const val1 = device.test_modules![key]; - if (!val1.enabled) { - return false; - } - } - - if (device.additional_info) { - for (const question of device.additional_info) { - if (question.answer !== '') { - return false; - } - } - } else { - return false; - } - return true; - } - - private setDialogHeight(): void { - const windowHeight = window.innerHeight; - if (windowHeight < this.FORM_HEIGHT) { - this.element.nativeElement.style.height = '100vh'; - } else { - this.element.nativeElement.style.height = this.FORM_HEIGHT + 'px'; - } } } diff --git a/modules/ui/src/app/pages/devices/devices.component.html b/modules/ui/src/app/pages/devices/devices.component.html index 28fb015db..73c56c628 100644 --- a/modules/ui/src/app/pages/devices/devices.component.html +++ b/modules/ui/src/app/pages/devices/devices.component.html @@ -22,15 +22,21 @@ [itemTemplate]="itemTemplate" [actions]="vm.actions" [entities]="vm.devices" - (addEntity)="openDialog(vm.devices, vm.testModules)" + (addEntity)="openForm()" (menuItemClicked)="menuItemClicked($event, vm.devices, vm.testModules)"> + @@ -44,7 +50,7 @@