From 8791d8f4066e4374800af14e388b64d80e5153e4 Mon Sep 17 00:00:00 2001 From: kurilova Date: Mon, 6 May 2024 09:06:09 +0000 Subject: [PATCH 1/3] Adds upload certificate --- modules/ui/src/app/app.component.ts | 1 - modules/ui/src/app/app.store.spec.ts | 8 +- modules/ui/src/app/app.store.ts | 22 +-- modules/ui/src/app/mocks/certificate.mock.ts | 5 + modules/ui/src/app/model/certificate.ts | 5 +- .../certificate-item.component.html | 10 +- .../certificate-item.component.scss | 14 +- .../certificate-item.component.spec.ts | 68 ++++++--- .../certificate-item.component.ts | 7 +- .../certificate-upload-button.component.html | 13 ++ .../certificate-upload-button.component.scss | 12 ++ ...ertificate-upload-button.component.spec.ts | 73 ++++++++++ .../certificate-upload-button.component.ts | 25 ++++ .../certificates/certificates.component.html | 57 ++++---- .../certificates/certificates.component.scss | 9 -- .../certificates.component.spec.ts | 14 +- .../certificates/certificates.component.ts | 27 +++- .../certificates/certificates.store.spec.ts | 131 ++++++++++++++++++ .../pages/certificates/certificates.store.ts | 110 +++++++++++++++ .../src/app/services/notification.service.ts | 4 +- .../src/app/services/test-run.service.spec.ts | 14 ++ .../ui/src/app/services/test-run.service.ts | 9 ++ modules/ui/src/styles.scss | 4 + 23 files changed, 537 insertions(+), 105 deletions(-) create mode 100644 modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.html create mode 100644 modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.scss create mode 100644 modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.spec.ts create mode 100644 modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.ts create mode 100644 modules/ui/src/app/pages/certificates/certificates.store.spec.ts create mode 100644 modules/ui/src/app/pages/certificates/certificates.store.ts diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 91a0ddd13..ba04304b4 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -74,7 +74,6 @@ export class AppComponent { ) { this.appStore.getDevices(); this.appStore.getSystemStatus(); - this.appStore.getCertificates(); this.matIconRegistry.addSvgIcon( 'devices', this.domSanitizer.bypassSecurityTrustResourceUrl(DEVICES_LOGO_URL) diff --git a/modules/ui/src/app/app.store.spec.ts b/modules/ui/src/app/app.store.spec.ts index ffdb18921..e3c0712c5 100644 --- a/modules/ui/src/app/app.store.spec.ts +++ b/modules/ui/src/app/app.store.spec.ts @@ -58,12 +58,7 @@ describe('AppStore', () => { let mockService: SpyObj; beforeEach(() => { - mockService = jasmine.createSpyObj([ - 'fetchDevices', - 'fetchSystemStatus', - 'fetchCertificates', - 'deleteCertificate', - ]); + mockService = jasmine.createSpyObj(['fetchDevices', 'fetchSystemStatus']); TestBed.configureTestingModule({ providers: [ @@ -137,7 +132,6 @@ describe('AppStore', () => { isMenuOpen: true, interfaces: {}, settingMissedError: null, - certificates: [], }); done(); }); diff --git a/modules/ui/src/app/app.store.ts b/modules/ui/src/app/app.store.ts index 46d42bd3f..63766fc59 100644 --- a/modules/ui/src/app/app.store.ts +++ b/modules/ui/src/app/app.store.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { ComponentStore } from '@ngrx/component-store'; -import { tap, withLatestFrom } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; import { selectError, selectHasConnectionSettings, @@ -52,7 +52,6 @@ export interface AppComponentState { export class AppStore extends ComponentStore { private consentShown$ = this.select(state => state.consentShown); private isStatusLoaded$ = this.select(state => state.isStatusLoaded); - private certificates$ = this.select(state => state.certificates); private hasDevices$ = this.store.select(selectHasDevices); private hasConnectionSetting$ = this.store.select( selectHasConnectionSettings @@ -75,7 +74,6 @@ export class AppStore extends ComponentStore { isMenuOpen: this.isMenuOpen$, interfaces: this.interfaces$, settingMissedError: this.settingMissedError$, - certificates: this.certificates$, }); updateConsent = this.updater((state, consentShown: boolean) => ({ @@ -88,11 +86,6 @@ export class AppStore extends ComponentStore { isStatusLoaded, })); - updateCertificates = this.updater((state, certificates: Certificate[]) => ({ - ...state, - certificates, - })); - setContent = this.effect(trigger$ => { return trigger$.pipe( tap(() => { @@ -137,18 +130,6 @@ export class AppStore extends ComponentStore { ); }); - getCertificates = this.effect(trigger$ => { - return trigger$.pipe( - exhaustMap(() => { - return this.testRunService.fetchCertificates().pipe( - tap((certificates: Certificate[]) => { - this.updateCertificates(certificates); - }) - ); - }) - ); - }); - deleteCertificate = this.effect(trigger$ => { return trigger$.pipe( exhaustMap((name: string) => { @@ -177,7 +158,6 @@ export class AppStore extends ComponentStore { isStatusLoaded: false, isTestrunStarted: false, systemStatus: null, - certificates: [], }); } } diff --git a/modules/ui/src/app/mocks/certificate.mock.ts b/modules/ui/src/app/mocks/certificate.mock.ts index f563d7d43..65d0be109 100644 --- a/modules/ui/src/app/mocks/certificate.mock.ts +++ b/modules/ui/src/app/mocks/certificate.mock.ts @@ -21,6 +21,11 @@ export const certificate = { expires: '2024-09-01T09:00:12Z', } as Certificate; +export const certificate_uploading = { + name: 'test', + uploading: true, +} as Certificate; + export const certificate2 = { name: 'sensor.bms.google.com', organisation: 'Google, Inc.', diff --git a/modules/ui/src/app/model/certificate.ts b/modules/ui/src/app/model/certificate.ts index b3abd24d6..384120cf7 100644 --- a/modules/ui/src/app/model/certificate.ts +++ b/modules/ui/src/app/model/certificate.ts @@ -15,6 +15,7 @@ */ export interface Certificate { name: string; - organisation: string; - expires: string; + organisation?: string; + expires?: string; + uploading?: boolean; } diff --git a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.html b/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.html index d1a1b2189..ee641becf 100644 --- a/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.html +++ b/modules/ui/src/app/pages/certificates/certificate-item/certificate-item.component.html @@ -4,13 +4,19 @@ >

{{ certificate.name }}

-

{{ certificate.organisation }}

-

+

+ {{ certificate.organisation }} +

+

{{ certificate.expires | date: 'dd MMM yyyy' }}

+
+ + diff --git a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.scss b/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.scss new file mode 100644 index 000000000..3f9e70f6e --- /dev/null +++ b/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.scss @@ -0,0 +1,12 @@ +.browse-files-button { + margin: 18px 16px; + padding: 8px 24px; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; +} + +#default-file-input { + display: none; +} diff --git a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.spec.ts b/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.spec.ts new file mode 100644 index 000000000..2dcbd9e05 --- /dev/null +++ b/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CertificateUploadButtonComponent } from './certificate-upload-button.component'; + +describe('CertificateUploadButtonComponent', () => { + let component: CertificateUploadButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CertificateUploadButtonComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CertificateUploadButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('DOM tests', () => { + it('should have upload file button', () => { + const uploadCertificatesButton = fixture.nativeElement.querySelector( + '.browse-files-button' + ) as HTMLButtonElement; + + expect(uploadCertificatesButton).toBeDefined(); + }); + + it('should have hidden file input', () => { + const input = fixture.nativeElement.querySelector( + '#default-file-input' + ) as HTMLInputElement; + + expect(input).toBeDefined(); + }); + + it('should emit input click on button click', () => { + const input = fixture.nativeElement.querySelector( + '#default-file-input' + ) as HTMLInputElement; + const inputSpy = spyOn(input, 'click'); + + const uploadCertificatesButton = fixture.nativeElement.querySelector( + '.browse-files-button' + ) as HTMLButtonElement; + + uploadCertificatesButton.click(); + + expect(inputSpy).toHaveBeenCalled(); + }); + + it('should detect file input change and emit event', () => { + const emitSpy = spyOn(component.fileChanged, 'emit'); + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(new File([''], 'test-file.pdf')); + + const inputDebugEl = fixture.nativeElement.querySelector( + '#default-file-input' + ) as HTMLInputElement; + + inputDebugEl.files = dataTransfer.files; + + inputDebugEl.dispatchEvent(new InputEvent('change')); + + fixture.detectChanges(); + + expect(emitSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.ts b/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.ts new file mode 100644 index 000000000..5b06163b4 --- /dev/null +++ b/modules/ui/src/app/pages/certificates/certificate-upload-button/certificate-upload-button.component.ts @@ -0,0 +1,25 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; + +@Component({ + selector: 'app-certificate-upload-button', + standalone: true, + imports: [MatButtonModule], + templateUrl: './certificate-upload-button.component.html', + styleUrl: './certificate-upload-button.component.scss', +}) +export class CertificateUploadButtonComponent { + @Output() fileChanged = new EventEmitter(); + fileChange(event: Event) { + const fileList = (event.target as HTMLInputElement).files; + + if (fileList && fileList.length < 1) { + return; + } + + // @ts-expect-error fileList is not null at this point + const file: File = fileList[0]; + + this.fileChanged.emit(file); + } +} diff --git a/modules/ui/src/app/pages/certificates/certificates.component.html b/modules/ui/src/app/pages/certificates/certificates.component.html index 0450a951c..ce2f8ae2e 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.html +++ b/modules/ui/src/app/pages/certificates/certificates.component.html @@ -13,34 +13,35 @@ See the License for the specific language governing permissions and limitations under the License. --> -
-

Certificates

- -
-
- -
- -
- +
+ +
+ +
+ +
+ diff --git a/modules/ui/src/app/pages/certificates/certificates.component.scss b/modules/ui/src/app/pages/certificates/certificates.component.scss index aa0903cde..eca247502 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.scss +++ b/modules/ui/src/app/pages/certificates/certificates.component.scss @@ -62,15 +62,6 @@ grid-template-rows: auto 1fr auto; } -.browse-files-button { - margin: 18px 16px; - padding: 8px 24px; - font-size: 14px; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.25px; -} - .content-certificates { padding: 0 16px; border-bottom: 1px solid $lighter-grey; diff --git a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts index 1b7554b70..2dfb75b9e 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.spec.ts +++ b/modules/ui/src/app/pages/certificates/certificates.component.spec.ts @@ -24,17 +24,29 @@ import SpyObj = jasmine.SpyObj; import { of } from 'rxjs'; import { MatDialogRef } from '@angular/material/dialog'; import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; +import { TestRunService } from '../../services/test-run.service'; describe('CertificatesComponent', () => { let component: CertificatesComponent; let mockLiveAnnouncer: SpyObj; + let mockService: SpyObj; let fixture: ComponentFixture; beforeEach(async () => { + mockService = jasmine.createSpyObj([ + 'fetchCertificates', + 'uploadCertificate', + ]); + mockService.fetchCertificates.and.returnValue( + of([certificate, certificate]) + ); mockLiveAnnouncer = jasmine.createSpyObj(['announce']); await TestBed.configureTestingModule({ imports: [CertificatesComponent, MatIconTestingModule, MatIcon], - providers: [{ provide: LiveAnnouncer, useValue: mockLiveAnnouncer }], + providers: [ + { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + { provide: TestRunService, useValue: mockService }, + ], }).compileComponents(); fixture = TestBed.createComponent(CertificatesComponent); diff --git a/modules/ui/src/app/pages/certificates/certificates.component.ts b/modules/ui/src/app/pages/certificates/certificates.component.ts index cd2dbc845..69a9067cd 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.ts +++ b/modules/ui/src/app/pages/certificates/certificates.component.ts @@ -22,10 +22,11 @@ import { } from '@angular/core'; import { MatIcon } from '@angular/material/icon'; import { CertificateItemComponent } from './certificate-item/certificate-item.component'; -import { NgForOf } from '@angular/common'; +import { CommonModule, DatePipe } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; -import { Certificate } from '../../model/certificate'; import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { CertificateUploadButtonComponent } from './certificate-upload-button/certificate-upload-button.component'; +import { CertificatesStore } from './certificates.store'; import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; import { Subject, takeUntil } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; @@ -33,12 +34,19 @@ import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'app-certificates', standalone: true, - imports: [MatIcon, CertificateItemComponent, NgForOf, MatButtonModule], + imports: [ + MatIcon, + CertificateItemComponent, + MatButtonModule, + CertificateUploadButtonComponent, + CommonModule, + ], + providers: [CertificatesStore, DatePipe], templateUrl: './certificates.component.html', styleUrl: './certificates.component.scss', }) export class CertificatesComponent implements OnDestroy { - @Input() certificates: Certificate[] = []; + viewModel$ = this.store.viewModel$; @Output() closeCertificatedEvent = new EventEmitter(); @Output() deleteCertificateEvent = new EventEmitter(); @@ -46,8 +54,11 @@ export class CertificatesComponent implements OnDestroy { constructor( private liveAnnouncer: LiveAnnouncer, - public dialog: MatDialog - ) {} + private store: CertificatesStore, + public dialog: MatDialog, + ) { + this.store.getCertificates(); + } ngOnDestroy() { this.destroy$.next(true); @@ -59,6 +70,10 @@ export class CertificatesComponent implements OnDestroy { this.closeCertificatedEvent.emit(); } + uploadFile(file: File) { + this.store.uploadCertificate(file); + } + deleteCertificate(name: string) { const dialogRef = this.dialog.open(DeleteFormComponent, { ariaLabel: 'Delete certificate', diff --git a/modules/ui/src/app/pages/certificates/certificates.store.spec.ts b/modules/ui/src/app/pages/certificates/certificates.store.spec.ts new file mode 100644 index 000000000..250edf4d4 --- /dev/null +++ b/modules/ui/src/app/pages/certificates/certificates.store.spec.ts @@ -0,0 +1,131 @@ +/* + * 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 { + certificate, + certificate_uploading, +} from '../../mocks/certificate.mock'; +import { CertificatesStore } from './certificates.store'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { DatePipe } from '@angular/common'; +import { NotificationService } from '../../services/notification.service'; + +describe('CertificatesStore', () => { + let certificateStore: CertificatesStore; + let mockService: SpyObj; + const notificationServiceMock: jasmine.SpyObj = + jasmine.createSpyObj(['notify']); + + beforeEach(() => { + mockService = jasmine.createSpyObj([ + 'fetchCertificates', + 'uploadCertificate', + ]); + + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule], + providers: [ + CertificatesStore, + provideMockStore({}), + { provide: TestRunService, useValue: mockService }, + { provide: NotificationService, useValue: notificationServiceMock }, + DatePipe, + ], + }); + + certificateStore = TestBed.inject(CertificatesStore); + }); + + it('should be created', () => { + expect(certificateStore).toBeTruthy(); + }); + + describe('updaters', () => { + it('should update certificates', (done: DoneFn) => { + certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.certificates).toEqual([certificate]); + done(); + }); + + certificateStore.updateCertificates([certificate]); + }); + }); + + describe('selectors', () => { + it('should select state', done => { + certificateStore.viewModel$.pipe(take(1)).subscribe(store => { + expect(store).toEqual({ + certificates: [], + }); + done(); + }); + }); + }); + + describe('effects', () => { + describe('fetchCertificates', () => { + const certificates = [certificate]; + + beforeEach(() => { + mockService.fetchCertificates.and.returnValue(of(certificates)); + }); + + it('should update certificates', done => { + certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.certificates).toEqual(certificates); + done(); + }); + + certificateStore.getCertificates(); + }); + }); + + describe('uploadCertificate', () => { + const uploadingCertificate = certificate_uploading; + + beforeEach(() => { + mockService.uploadCertificate.and.returnValue(of(true)); + mockService.fetchCertificates.and.returnValue(of([certificate])); + }); + + it('should update certificates', done => { + certificateStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { + expect(store.certificates).toContain(uploadingCertificate); + }); + + certificateStore.viewModel$.pipe(skip(2), take(1)).subscribe(store => { + expect(store.certificates).toEqual([certificate]); + done(); + }); + + certificateStore.uploadCertificate(new File([], 'test')); + }); + + it('should notify', () => { + certificateStore.uploadCertificate(new File([], 'test')); + expect(notificationServiceMock.notify).toHaveBeenCalledWith( + 'Certificate successfully added.\niot.bms.google.com by Google, Inc. valid until 01 Sep 2024', + 0, + 'certificate-notification' + ); + }); + }); + }); +}); diff --git a/modules/ui/src/app/pages/certificates/certificates.store.ts b/modules/ui/src/app/pages/certificates/certificates.store.ts new file mode 100644 index 000000000..94685383e --- /dev/null +++ b/modules/ui/src/app/pages/certificates/certificates.store.ts @@ -0,0 +1,110 @@ +/* + * 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, withLatestFrom } from 'rxjs/operators'; +import { catchError, EMPTY, exhaustMap, throwError } from 'rxjs'; +import { Certificate } from '../../model/certificate'; +import { TestRunService } from '../../services/test-run.service'; +import { NotificationService } from '../../services/notification.service'; +import { DatePipe } from '@angular/common'; + +export interface AppComponentState { + certificates: Certificate[]; +} +@Injectable() +export class CertificatesStore extends ComponentStore { + private certificates$ = this.select(state => state.certificates); + + viewModel$ = this.select({ + certificates: this.certificates$, + }); + + updateCertificates = this.updater((state, certificates: Certificate[]) => ({ + ...state, + certificates, + })); + + getCertificates = this.effect(trigger$ => { + return trigger$.pipe( + exhaustMap(() => { + return this.testRunService.fetchCertificates().pipe( + tap((certificates: Certificate[]) => { + this.updateCertificates(certificates); + }) + ); + }) + ); + }); + + uploadCertificate = this.effect(trigger$ => { + return trigger$.pipe( + withLatestFrom(this.certificates$), + tap(([file, certificates]) => { + this.addCertificate(file.name, certificates); + }), + exhaustMap(([file, certificates]) => { + return this.testRunService.uploadCertificate(file).pipe( + exhaustMap(uploaded => { + if (uploaded) { + return this.testRunService.fetchCertificates(); + } + return throwError('Failed to upload certificate'); + }), + tap(newCertificates => { + const uploadedCertificate = newCertificates.filter( + certificate => + !certificates.some(cert => cert.name === certificate.name) + )[0]; + this.updateCertificates(newCertificates); + this.notificationService.notify( + `Certificate successfully added.\n${uploadedCertificate.name} by ${uploadedCertificate.organisation} valid until ${this.datePipe.transform(uploadedCertificate.expires, 'dd MMM yyyy')}`, + 0, + 'certificate-notification' + ); + }), + catchError(() => { + this.removeCertificate(file.name, certificates); + return EMPTY; + }) + ); + }) + ); + }); + + addCertificate(name: string, certificates: Certificate[]) { + const certificate = { name, uploading: true } as Certificate; + this.updateCertificates([certificate, ...certificates]); + } + + private removeCertificate(name: string, current: Certificate[]) { + const certificates = current.filter( + certificate => certificate.name !== name + ); + this.updateCertificates(certificates); + } + + constructor( + private testRunService: TestRunService, + private notificationService: NotificationService, + private datePipe: DatePipe + ) { + super({ + certificates: [], + }); + } +} diff --git a/modules/ui/src/app/services/notification.service.ts b/modules/ui/src/app/services/notification.service.ts index 3a068b4e7..aab437c97 100644 --- a/modules/ui/src/app/services/notification.service.ts +++ b/modules/ui/src/app/services/notification.service.ts @@ -36,10 +36,10 @@ export class NotificationService { private focusManagerService: FocusManagerService ) {} - notify(message: string, duration = 0) { + notify(message: string, duration = 0, panelClass = 'test-run-notification') { this.snackBarRef = this.snackBar.open(message, 'OK', { horizontalPosition: 'center', - panelClass: 'test-run-notification', + panelClass: panelClass, duration: duration, politeness: 'assertive', }); 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 4bf137dbb..1c54810a6 100644 --- a/modules/ui/src/app/services/test-run.service.spec.ts +++ b/modules/ui/src/app/services/test-run.service.spec.ts @@ -476,6 +476,20 @@ describe('TestRunService', () => { req.flush(certificates); }); + it('uploadCertificates should upload certificate', () => { + service.uploadCertificate(new File([], 'test')).subscribe(res => { + expect(res).toEqual(true); + }); + + const req = httpTestingController.expectOne( + 'http://localhost:8000/system/config/certs/upload' + ); + + expect(req.request.method).toBe('POST'); + + req.flush(true); + }); + it('deleteCertificate should delete certificate', () => { service.deleteCertificate('test').subscribe(res => { expect(res).toEqual(true); diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index daa409125..719f0cfa3 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -230,4 +230,13 @@ export class TestRunService { }) .pipe(map(() => true)); } + + uploadCertificate(file: File): Observable { + const formData: FormData = new FormData(); + formData.append('file', file, file.name); + formData.append('mode', 'file'); + return this.http + .post(`${API_URL}/system/config/certs/upload`, formData) + .pipe(map(() => true)); + } } diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss index 49863c3c3..c0c058ef6 100644 --- a/modules/ui/src/styles.scss +++ b/modules/ui/src/styles.scss @@ -272,3 +272,7 @@ body opacity: var(--mat-text-button-focus-state-layer-opacity); } } + +.certificate-notification .mat-mdc-snack-bar-label { + white-space: pre-line; +} From ffd3ff7ea38d645a0bc166c01abcf91c07e8f51c Mon Sep 17 00:00:00 2001 From: kurilova Date: Mon, 6 May 2024 11:42:06 +0000 Subject: [PATCH 2/3] move delete functionality to certificates store --- modules/ui/src/app/app.component.html | 2 - modules/ui/src/app/app.component.spec.ts | 12 ++---- modules/ui/src/app/app.component.ts | 4 -- modules/ui/src/app/app.store.spec.ts | 42 ------------------- modules/ui/src/app/app.store.ts | 21 ---------- .../certificates/certificates.component.html | 4 +- .../certificates.component.spec.ts | 7 +--- .../certificates/certificates.component.ts | 13 ++---- .../certificates/certificates.store.spec.ts | 17 ++++++++ .../pages/certificates/certificates.store.ts | 13 ++++++ 10 files changed, 42 insertions(+), 93 deletions(-) diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html index f47583973..ef889d2c4 100644 --- a/modules/ui/src/app/app.component.html +++ b/modules/ui/src/app/app.component.html @@ -206,8 +206,6 @@

Testrun

autoFocus="#setting-panel-close-button" class="settings-drawer"> diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index b6b577984..26ee57183 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -63,6 +63,7 @@ import { import { MatIconTestingModule } from '@angular/material/icon/testing'; import { CertificatesComponent } from './pages/certificates/certificates.component'; import { of } from 'rxjs'; +import {certificate} from './mocks/certificate.mock'; describe('AppComponent', () => { let component: AppComponent; @@ -97,10 +98,11 @@ describe('AppComponent', () => { 'getTestModules', 'testrunInProgress', 'fetchCertificates', - 'deleteCertificate', ]); - mockService.deleteCertificate.and.returnValue(of(true)); + mockService.fetchCertificates.and.returnValue( + of([]) + ); mockFocusManagerService = jasmine.createSpyObj('mockFocusManagerService', [ 'focusFirstElementInContainer', ]); @@ -676,12 +678,6 @@ describe('AppComponent', () => { expect(component.certDrawer.open).toHaveBeenCalledTimes(1); }); - - it('should call delete certificate', () => { - component.deleteCertificate('name'); - - expect(mockService.deleteCertificate).toHaveBeenCalledWith('name'); - }); }); @Component({ diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index ba04304b4..ea315f68b 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -164,8 +164,4 @@ export class AppComponent { isTestrunInProgress(status?: string) { return this.testRunService.testrunInProgress(status); } - - deleteCertificate(name: string) { - this.appStore.deleteCertificate(name); - } } diff --git a/modules/ui/src/app/app.store.spec.ts b/modules/ui/src/app/app.store.spec.ts index e3c0712c5..d53840850 100644 --- a/modules/ui/src/app/app.store.spec.ts +++ b/modules/ui/src/app/app.store.spec.ts @@ -32,7 +32,6 @@ import SpyObj = jasmine.SpyObj; import { device } from './mocks/device.mock'; import { setDevices, setTestrunStatus } from './store/actions'; import { MOCK_PROGRESS_DATA_IN_PROGRESS } from './mocks/progress.mock'; -import { certificate, certificate2 } from './mocks/certificate.mock'; const mock = (() => { let store: { [key: string]: string } = {}; @@ -108,15 +107,6 @@ describe('AppStore', () => { appStore.updateIsStatusLoaded(true); }); - - it('should update certificates', (done: DoneFn) => { - appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.certificates).toEqual([certificate]); - done(); - }); - - appStore.updateCertificates([certificate]); - }); }); describe('selectors', () => { @@ -194,37 +184,5 @@ describe('AppStore', () => { appStore.getSystemStatus(); }); }); - - describe('fetchCertificates', () => { - const certificates = [certificate]; - - beforeEach(() => { - mockService.fetchCertificates.and.returnValue(of(certificates)); - }); - - it('should update certificates', done => { - appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.certificates).toEqual(certificates); - done(); - }); - - appStore.getCertificates(); - }); - }); - - describe('deleteCertificate', () => { - it('should update store', done => { - mockService.deleteCertificate.and.returnValue(of(true)); - - appStore.updateCertificates([certificate, certificate2]); - - appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => { - expect(store.certificates).toEqual([certificate2]); - done(); - }); - - appStore.deleteCertificate('iot.bms.google.com'); - }); - }); }); }); diff --git a/modules/ui/src/app/app.store.ts b/modules/ui/src/app/app.store.ts index 63766fc59..d214d4848 100644 --- a/modules/ui/src/app/app.store.ts +++ b/modules/ui/src/app/app.store.ts @@ -38,7 +38,6 @@ import { } from './store/actions'; import { TestrunStatus } from './model/testrun-status'; import { SettingMissedError, SystemInterfaces } from './model/setting'; -import { Certificate } from './model/certificate'; export const CONSENT_SHOWN_KEY = 'CONSENT_SHOWN'; export interface AppComponentState { @@ -46,7 +45,6 @@ export interface AppComponentState { isStatusLoaded: boolean; isTestrunStarted: boolean; systemStatus: TestrunStatus | null; - certificates: Certificate[]; } @Injectable() export class AppStore extends ComponentStore { @@ -130,25 +128,6 @@ export class AppStore extends ComponentStore { ); }); - deleteCertificate = this.effect(trigger$ => { - return trigger$.pipe( - exhaustMap((name: string) => { - return this.testRunService.deleteCertificate(name).pipe( - withLatestFrom(this.certificates$), - tap(([, current]) => { - this.removeCertificate(name, current); - }) - ); - }) - ); - }); - - private removeCertificate(name: string, current: Certificate[]) { - const certificates = current.filter( - certificate => certificate.name !== name - ); - this.updateCertificates(certificates); - } constructor( private store: Store, private testRunService: TestRunService diff --git a/modules/ui/src/app/pages/certificates/certificates.component.html b/modules/ui/src/app/pages/certificates/certificates.component.html index ce2f8ae2e..23781d330 100644 --- a/modules/ui/src/app/pages/certificates/certificates.component.html +++ b/modules/ui/src/app/pages/certificates/certificates.component.html @@ -32,7 +32,9 @@

Certificates

+ (deleteButtonClicked)=" + deleteCertificate($event) + ">