From 401f831584cf244d1fa2f997726b176bc8c126ac Mon Sep 17 00:00:00 2001 From: Volha Mardvilka Date: Thu, 16 May 2024 09:20:03 +0000 Subject: [PATCH] 339311887: (feat) display saved risk profile --- modules/ui/src/app/mocks/profile.mock.ts | 22 +++++ modules/ui/src/app/model/profile.ts | 30 +++++++ .../profile-item/profile-item.component.html | 30 +++++++ .../profile-item/profile-item.component.scss | 57 ++++++++++++ .../profile-item.component.spec.ts | 47 ++++++++++ .../profile-item/profile-item.component.ts | 32 +++++++ .../risk-assessment.component.html | 28 +++++- .../risk-assessment.component.scss | 39 ++++++++ .../risk-assessment.component.spec.ts | 71 ++++++++++++--- .../risk-assessment.component.ts | 9 +- .../risk-assessment/risk-assessment.module.ts | 10 ++- .../risk-assessment.store.spec.ts | 88 +++++++++++++++++++ .../risk-assessment/risk-assessment.store.ts | 56 ++++++++++++ .../src/app/services/test-run.service.spec.ts | 15 ++++ .../ui/src/app/services/test-run.service.ts | 5 ++ 15 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 modules/ui/src/app/mocks/profile.mock.ts create mode 100644 modules/ui/src/app/model/profile.ts create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts create mode 100644 modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts create mode 100644 modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts create mode 100644 modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts diff --git a/modules/ui/src/app/mocks/profile.mock.ts b/modules/ui/src/app/mocks/profile.mock.ts new file mode 100644 index 000000000..d5472a4f6 --- /dev/null +++ b/modules/ui/src/app/mocks/profile.mock.ts @@ -0,0 +1,22 @@ +/** + * 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 { Profile } from '../model/profile'; + +export const PROFILE_MOCK: Profile = { + name: 'Profile name', + sections: [], +}; diff --git a/modules/ui/src/app/model/profile.ts b/modules/ui/src/app/model/profile.ts new file mode 100644 index 000000000..28bf4bd1c --- /dev/null +++ b/modules/ui/src/app/model/profile.ts @@ -0,0 +1,30 @@ +/** + * 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. + */ +interface ProfileResponse { + question: string; + type: string; + options: string[]; +} + +interface ProfileSection { + name: string; + responses: ProfileResponse[]; +} + +export interface Profile { + name: string; + sections: ProfileSection[]; +} diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html new file mode 100644 index 000000000..a3549c47b --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.html @@ -0,0 +1,30 @@ + +
+ + rule + +

{{ profile.name }}

+ + +
diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss new file mode 100644 index 000000000..1b745a68b --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.scss @@ -0,0 +1,57 @@ +/** + * 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'; + +.profile-item-container { + display: grid; + grid-template-columns: 24px minmax(160px, 1fr) 24px 24px; + gap: 16px; + box-sizing: border-box; + height: 68px; + padding: 12px 0; + border-bottom: 1px solid $lighter-grey; + align-items: center; +} + +.profile-item-icon { + color: $grey-700; +} + +.profile-item-name { + margin: 0; + font-family: $font-secondary, sans-serif; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 16px; + color: $grey-800; + height: 24px; +} + +.profile-item-button { + padding: 0; + height: 24px; + width: 24px; + border-radius: 4px; + color: $grey-700; + display: flex; + align-items: flex-start; + justify-content: center; + & ::ng-deep .mat-mdc-button-persistent-ripple { + border-radius: 4px; + } +} diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts new file mode 100644 index 000000000..03463c0c7 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.spec.ts @@ -0,0 +1,47 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfileItemComponent } from './profile-item.component'; +import { PROFILE_MOCK } from '../../../mocks/profile.mock'; + +describe('ProfileItemComponent', () => { + let component: ProfileItemComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfileItemComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ProfileItemComponent); + component = fixture.componentInstance; + component.profile = PROFILE_MOCK; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have profile name', () => { + const name = compiled.querySelector('.profile-item-name'); + + expect(name?.textContent?.trim()).toEqual('Profile name'); + }); +}); diff --git a/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts new file mode 100644 index 000000000..4e34d7b56 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/profile-item/profile-item.component.ts @@ -0,0 +1,32 @@ +/* + * 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 { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { Profile } from '../../../model/profile'; +import { MatIcon } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-profile-item', + standalone: true, + imports: [MatIcon, MatButtonModule, CommonModule], + templateUrl: './profile-item.component.html', + styleUrl: './profile-item.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProfileItemComponent { + @Input() profile!: Profile; +} diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html index e19456aa5..eacb5ad7d 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.html @@ -13,6 +13,28 @@ See the License for the specific language governing permissions and limitations under the License. --> - -

Risk assessment

-
+ + + + +

Risk assessment

+
+
+
+ +
+

Saved profiles

+
+
+ + +
+
+
diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss index 171771e81..82554f70a 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.scss @@ -15,8 +15,47 @@ */ @import 'src/theming/colors'; +.risk-assessment-container, +.risk-assessment-content { + height: 100%; + background-color: $white; +} + +.risk-assessment-content { + display: flex; + flex-direction: column; + gap: 14px; +} + .risk-assessment-toolbar { height: 74px; padding: 24px 0 8px 32px; background: $white; } + +.main-content { + padding: 16px 32px; +} + +.profiles-drawer { + width: 320px; + box-shadow: none; + border-left: 1px solid $light-grey; +} + +.profiles-drawer-header { + padding: 12px 12px 16px 24px; +} + +.profiles-drawer-header-title { + margin: 0; + font-size: 22px; + font-style: normal; + font-weight: 400; + line-height: 28px; + color: $dark-grey; +} + +.profiles-drawer-content { + padding: 0 16px; +} diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts index dc1f931e1..e1a15a375 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.spec.ts @@ -18,35 +18,86 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RiskAssessmentComponent } from './risk-assessment.component'; import { MatToolbarModule } from '@angular/material/toolbar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { TestRunService } from '../../services/test-run.service'; +import SpyObj = jasmine.SpyObj; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { PROFILE_MOCK } from '../../mocks/profile.mock'; +import { of } from 'rxjs'; +import { Component, Input } from '@angular/core'; +import { Profile } from '../../model/profile'; describe('RiskAssessmentComponent', () => { let component: RiskAssessmentComponent; let fixture: ComponentFixture; + let mockService: SpyObj; let compiled: HTMLElement; beforeEach(async () => { + mockService = jasmine.createSpyObj(['fetchProfiles']); + await TestBed.configureTestingModule({ - declarations: [RiskAssessmentComponent], - imports: [MatToolbarModule, BrowserAnimationsModule], + declarations: [RiskAssessmentComponent, FakeProfileItemComponent], + imports: [MatToolbarModule, MatSidenavModule, BrowserAnimationsModule], + providers: [{ provide: TestRunService, useValue: mockService }], }).compileComponents(); fixture = TestBed.createComponent(RiskAssessmentComponent); component = fixture.componentInstance; compiled = fixture.nativeElement as HTMLElement; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); - it('should have toolbar with title', () => { - const toolbarEl = compiled.querySelector('.risk-assessment-toolbar'); - const title = compiled.querySelector('h2.title'); - const titleContent = title?.innerHTML.trim(); + describe('with no data', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should have toolbar with title', () => { + const toolbarEl = compiled.querySelector('.risk-assessment-toolbar'); + const title = compiled.querySelector('h2.title'); + const titleContent = title?.innerHTML.trim(); + + expect(toolbarEl).not.toBeNull(); + expect(title).toBeTruthy(); + expect(titleContent).toContain('Risk assessment'); + }); - expect(toolbarEl).not.toBeNull(); - expect(title).toBeTruthy(); - expect(titleContent).toContain('Risk assessment'); + it('should not have profiles drawer', () => { + const profilesDrawer = compiled.querySelector('.profiles-drawer'); + + expect(profilesDrawer).toBeFalsy(); + }); + }); + + describe('with profiles data', () => { + beforeEach(() => { + component.viewModel$ = of({ + profiles: [PROFILE_MOCK, PROFILE_MOCK], + }); + fixture.detectChanges(); + }); + + it('should have profiles drawer', () => { + const profilesDrawer = compiled.querySelector('.profiles-drawer'); + + expect(profilesDrawer).toBeTruthy(); + }); + + it('should have profile items', () => { + const profileItems = compiled.querySelectorAll('app-profile-item'); + + expect(profileItems.length).toEqual(2); + }); }); }); + +@Component({ + selector: 'app-profile-item', + template: '
', +}) +class FakeProfileItemComponent { + @Input() profile!: Profile; +} diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts index c788ae2c0..b9c805a65 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.component.ts @@ -14,11 +14,18 @@ * limitations under the License. */ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RiskAssessmentStore } from './risk-assessment.store'; @Component({ selector: 'app-risk-assessment', templateUrl: './risk-assessment.component.html', styleUrl: './risk-assessment.component.scss', + providers: [RiskAssessmentStore], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RiskAssessmentComponent {} +export class RiskAssessmentComponent { + viewModel$ = this.store.viewModel$; + constructor(private store: RiskAssessmentStore) { + this.store.getProfiles(); + } +} diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts index 6d3838a92..706760296 100644 --- a/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.module.ts @@ -19,9 +19,17 @@ import { CommonModule } from '@angular/common'; import { RiskAssessmentRoutingModule } from './risk-assessment-routing.module'; import { MatToolbarModule } from '@angular/material/toolbar'; import { RiskAssessmentComponent } from './risk-assessment.component'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { ProfileItemComponent } from './profile-item/profile-item.component'; @NgModule({ declarations: [RiskAssessmentComponent], - imports: [CommonModule, RiskAssessmentRoutingModule, MatToolbarModule], + imports: [ + CommonModule, + RiskAssessmentRoutingModule, + MatToolbarModule, + MatSidenavModule, + ProfileItemComponent, + ], }) export class RiskAssessmentModule {} diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts new file mode 100644 index 000000000..bd1df47d0 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.spec.ts @@ -0,0 +1,88 @@ +/* + * 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 { TestBed } from '@angular/core/testing'; +import { of, skip, take } from 'rxjs'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TestRunService } from '../../services/test-run.service'; +import SpyObj = jasmine.SpyObj; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RiskAssessmentStore } from './risk-assessment.store'; +import { PROFILE_MOCK } from '../../mocks/profile.mock'; + +describe('RiskAssessmentStore', () => { + let riskAssessmentStore: RiskAssessmentStore; + let mockService: SpyObj; + + beforeEach(() => { + mockService = jasmine.createSpyObj(['fetchProfiles']); + + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule], + providers: [ + RiskAssessmentStore, + provideMockStore({}), + { provide: TestRunService, useValue: mockService }, + ], + }); + + riskAssessmentStore = TestBed.inject(RiskAssessmentStore); + }); + + it('should be created', () => { + expect(riskAssessmentStore).toBeTruthy(); + }); + + describe('updaters', () => { + it('should update profiles', (done: DoneFn) => { + riskAssessmentStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.profiles).toEqual([PROFILE_MOCK]); + done(); + }); + + riskAssessmentStore.updateProfiles([PROFILE_MOCK]); + }); + }); + + describe('selectors', () => { + it('should select state', done => { + riskAssessmentStore.viewModel$.pipe(take(1)).subscribe(store => { + expect(store).toEqual({ + profiles: [], + }); + done(); + }); + }); + }); + + describe('effects', () => { + describe('getProfiles', () => { + beforeEach(() => { + mockService.fetchProfiles.and.returnValue(of([PROFILE_MOCK])); + }); + + it('should update profiles', done => { + riskAssessmentStore.viewModel$ + .pipe(skip(1), take(1)) + .subscribe(store => { + expect(store.profiles).toEqual([PROFILE_MOCK]); + done(); + }); + + riskAssessmentStore.getProfiles(); + }); + }); + }); +}); diff --git a/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts new file mode 100644 index 000000000..0f358d9c8 --- /dev/null +++ b/modules/ui/src/app/pages/risk-assessment/risk-assessment.store.ts @@ -0,0 +1,56 @@ +/* + * 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 { Injectable } from '@angular/core'; +import { ComponentStore } from '@ngrx/component-store'; +import { tap } from 'rxjs/operators'; +import { exhaustMap } from 'rxjs'; +import { TestRunService } from '../../services/test-run.service'; +import { Profile } from '../../model/profile'; + +export interface AppComponentState { + profiles: Profile[]; +} +@Injectable() +export class RiskAssessmentStore extends ComponentStore { + private profiles$ = this.select(state => state.profiles); + + viewModel$ = this.select({ + profiles: this.profiles$, + }); + + updateProfiles = this.updater((state, profiles: Profile[]) => ({ + ...state, + profiles, + })); + + getProfiles = this.effect(trigger$ => { + return trigger$.pipe( + exhaustMap(() => { + return this.testRunService.fetchProfiles().pipe( + tap((profiles: Profile[]) => { + this.updateProfiles(profiles); + }) + ); + }) + ); + }); + constructor(private testRunService: TestRunService) { + super({ + profiles: [], + }); + } +} diff --git a/modules/ui/src/app/services/test-run.service.spec.ts b/modules/ui/src/app/services/test-run.service.spec.ts index 32df1b5db..2125e0553 100644 --- a/modules/ui/src/app/services/test-run.service.spec.ts +++ b/modules/ui/src/app/services/test-run.service.spec.ts @@ -34,6 +34,7 @@ import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { AppState } from '../store/state'; import { Certificate } from '../model/certificate'; import { certificate } from '../mocks/certificate.mock'; +import { PROFILE_MOCK } from '../mocks/profile.mock'; const MOCK_SYSTEM_CONFIG: SystemConfig = { network: { @@ -478,6 +479,20 @@ describe('TestRunService', () => { req.flush(true); }); + it('fetchProfiles should return profiles', () => { + service.fetchProfiles().subscribe(res => { + expect(res).toEqual([PROFILE_MOCK]); + }); + + const req = httpTestingController.expectOne( + 'http://localhost:8000/profiles' + ); + + expect(req.request.method).toBe('GET'); + + req.flush([PROFILE_MOCK]); + }); + it('fetchCertificates should return certificates', () => { const certificates = [certificate] as Certificate[]; diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index ab751123c..dc4c0631c 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -28,6 +28,7 @@ import { } from '../model/testrun-status'; import { Version } from '../model/version'; import { Certificate } from '../model/certificate'; +import { Profile } from '../model/profile'; const API_URL = `http://${window.location.hostname}:8000`; export const SYSTEM_STOP = '/system/stop'; @@ -217,6 +218,10 @@ export class TestRunService { ); } + fetchProfiles(): Observable { + return this.http.get(`${API_URL}/profiles`); + } + fetchCertificates(): Observable { return this.http.get(`${API_URL}/system/config/certs/list`); }