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`);
}