+ + Selected port is missing! Please define a valid one using + Settings + panel. + + *ngIf="(hasConnectionSetting$ | async) === false"> Step 1: To perform a device test, please, select ports in Testrun [type]="CalloutType.Info" *ngIf=" (hasConnectionSetting$ | async) === true && - !(devices$ | async)?.length && - isDevicesLoaded + !(devices$ | async)?.length "> Step 2: To perform a device test please Testrun (devices$ | async)?.length && (!(systemStatus$ | async)?.status || (systemStatus$ | async)?.status === StatusOfTestrun.Idle) && - isStatusLoaded && + isStatusLoaded === true && (isTestrunStarted$ | async) === false "> Step 3: Once device is created navigate to @@ -151,7 +166,8 @@

Testrun

position="end" class="settings-drawer"> diff --git a/modules/ui/src/app/app.component.scss b/modules/ui/src/app/app.component.scss index 9eb33e232..f60d3a529 100644 --- a/modules/ui/src/app/app.component.scss +++ b/modules/ui/src/app/app.component.scss @@ -40,7 +40,7 @@ $nav-open-btn-width: 210px; margin-top: $toolbar-height; } -:host.active-menu { +.active-menu { .app-sidebar { width: $nav-open-width; align-items: start; diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 093ad3f35..787155bdd 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -45,7 +45,12 @@ import { } from './mocks/progress.mock'; import { LoaderService } from './services/loader.service'; import { Routes } from './model/routes'; -import { StateService } from './services/state.service'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { State } from '@ngrx/store'; +import { appFeatureKey } from './store/reducers'; +import { FocusManagerService } from './services/focus-manager.service'; +import { AppState } from './store/state'; +import { toggleMenu, updateFocusNavigation } from './store/actions'; describe('AppComponent', () => { let component: AppComponent; @@ -54,7 +59,9 @@ describe('AppComponent', () => { let router: Router; let mockService: SpyObj; let mockLoaderService: SpyObj; - let mockStateService: SpyObj; + let store: MockStore; + let focusNavigation = true; + let mockFocusManagerService: SpyObj; const enterKeyEvent = new KeyboardEvent('keydown', { key: 'Enter', @@ -79,22 +86,19 @@ describe('AppComponent', () => { 'setIsOpenAddDevice', 'systemStatus$', 'isTestrunStarted$', - 'hasConnectionSetting$', 'setIsOpenStartTestrun', ]); mockLoaderService = jasmine.createSpyObj(['setLoading']); - mockStateService = jasmine.createSpyObj('mockStateService', [ - 'focusFirstElementInMain', + mockFocusManagerService = jasmine.createSpyObj('mockFocusManagerService', [ + 'focusFirstElementInContainer', ]); mockService.getDevices.and.returnValue( new BehaviorSubject([device]) ); - mockService.getSystemInterfaces.and.returnValue(of({})); (mockService.systemStatus$ as unknown) = of({}); mockService.isTestrunStarted$ = of(true); - mockService.hasConnectionSetting$ = of(true); TestBed.configureTestingModule({ imports: [ @@ -112,7 +116,20 @@ describe('AppComponent', () => { providers: [ { provide: TestRunService, useValue: mockService }, { provide: LoaderService, useValue: mockLoaderService }, - { provide: StateService, useValue: mockStateService }, + { + provide: State, + useValue: { + getValue: () => ({ + [appFeatureKey]: { + appComponent: { + focusNavigation: focusNavigation, + }, + }, + }), + }, + }, + provideMockStore({}), + { provide: FocusManagerService, useValue: mockFocusManagerService }, ], declarations: [ AppComponent, @@ -122,11 +139,14 @@ describe('AppComponent', () => { ], }); + store = TestBed.inject(MockStore); fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; router = TestBed.get(Router); + component.hasConnectionSetting$ = of(true); fixture.detectChanges(); compiled = fixture.nativeElement as HTMLElement; + spyOn(store, 'dispatch').and.callFake(() => {}); }); it('should create the app', () => { @@ -233,7 +253,7 @@ describe('AppComponent', () => { }); })); - it('should call focusFirstElementInMain if settingsDrawer opened not from toggleBtn', fakeAsync(() => { + it('should call focusFirstElementInContainer if settingsDrawer opened not from toggleBtn', fakeAsync(() => { spyOn(component.settingsDrawer, 'close').and.returnValue( Promise.resolve('close') ); @@ -244,7 +264,9 @@ describe('AppComponent', () => { flush(); component.settingsDrawer.close().then(() => { - expect(mockStateService.focusFirstElementInMain).toHaveBeenCalled(); + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalled(); }); })); @@ -275,53 +297,41 @@ describe('AppComponent', () => { }); describe('menu button', () => { - it('should toggle menu open state on click', () => { + it('should dispatch toggleMenu action', () => { const menuBtn = compiled.querySelector( '.app-toolbar-button-menu' ) as HTMLButtonElement; menuBtn.click(); - expect(component.isMenuOpen).toBeTrue(); - - menuBtn.click(); - - expect(component.isMenuOpen).toBeFalse(); - }); - - it('should set flag focusNavigation if menu opens on click', () => { - component.isMenuOpen = false; - const menuBtn = compiled.querySelector( - '.app-toolbar-button-menu' - ) as HTMLButtonElement; - - menuBtn.click(); - - expect(component.focusNavigation).toBeTrue(); + expect(store.dispatch).toHaveBeenCalledWith(toggleMenu()); }); it('should focus navigation on tab press if menu button was clicked', () => { + focusNavigation = true; const menuBtn = compiled.querySelector( '.app-toolbar-button-menu' ) as HTMLButtonElement; - menuBtn.click(); menuBtn.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); - const navigationButton = compiled.querySelectorAll('.app-sidebar-button'); - - expect(component.focusNavigation).toBeFalse(); - expect(document.activeElement).toBe(navigationButton[0]); + const navigation = compiled.querySelector('.app-sidebar'); + + expect(store.dispatch).toHaveBeenCalledWith( + updateFocusNavigation({ focusNavigation: false }) + ); + expect( + mockFocusManagerService.focusFirstElementInContainer + ).toHaveBeenCalledWith(navigation); }); it('should not focus navigation button on tab press if menu button was not clicked', () => { - component.focusNavigation = false; + focusNavigation = false; const menuBtn = compiled.querySelector( '.app-toolbar-button-menu' ) as HTMLButtonElement; menuBtn.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); - expect(component.focusNavigation).toBeFalse(); expect(document.activeElement).toBe(document.body); }); }); @@ -347,7 +357,7 @@ describe('AppComponent', () => { describe('Callout component visibility', () => { describe('with no connection settings', () => { beforeEach(() => { - mockService.hasConnectionSetting$ = of(false); + component.hasConnectionSetting$ = of(false); component.ngOnInit(); fixture.detectChanges(); }); @@ -387,7 +397,7 @@ describe('AppComponent', () => { describe('with system status as "Idle"', () => { beforeEach(() => { - mockService.hasConnectionSetting$ = of(true); + component.hasConnectionSetting$ = of(true); mockService.getDevices.and.returnValue( new BehaviorSubject([device]) ); @@ -541,6 +551,36 @@ describe('AppComponent', () => { expect(callout).toBeNull(); }); }); + + describe('error', () => { + describe('with error', () => { + beforeEach(() => { + component.error$ = of(true); + component.ngOnInit(); + fixture.detectChanges(); + }); + it('should have callout component', () => { + const callout = compiled.querySelector('app-callout'); + const calloutContent = callout?.innerHTML.trim(); + + expect(callout).toBeTruthy(); + expect(calloutContent).toContain('Selected port is missing!'); + }); + }); + + describe('with no error', () => { + beforeEach(() => { + component.error$ = of(false); + component.ngOnInit(); + fixture.detectChanges(); + }); + it('should not have callout component', () => { + const callout = compiled.querySelector('app-callout'); + + expect(callout).toBeNull(); + }); + }); + }); }); it('should not call toggleSettingsBtn focus on closeSetting when device length is 0', async () => { @@ -567,6 +607,7 @@ describe('AppComponent', () => { }) class FakeGeneralSettingsComponent { @Input() interfaces = []; + @Input() hasConnectionSettings = false; @Output() closeSettingEvent = new EventEmitter(); @Output() reloadInterfacesEvent = new EventEmitter(); } diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index 287d8ba20..29d4a21dc 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -13,28 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Component, - ElementRef, - HostBinding, - OnInit, - ViewChild, -} from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { MatDrawer } from '@angular/material/sidenav'; import { SystemInterfaces, TestRunService } from './services/test-run.service'; -import { Observable } from 'rxjs/internal/Observable'; +import { Observable } from 'rxjs'; import { Device } from './model/device'; -import { take } from 'rxjs'; import { TestrunStatus, StatusOfTestrun } from './model/testrun-status'; import { Router } from '@angular/router'; import { LoaderService } from './services/loader.service'; import { CalloutType } from './model/callout-type'; -import { tap } from 'rxjs/internal/operators/tap'; -import { shareReplay } from 'rxjs/internal/operators/shareReplay'; +import { tap, shareReplay } from 'rxjs/operators'; import { Routes } from './model/routes'; -import { StateService } from './services/state.service'; +import { FocusManagerService } from './services/focus-manager.service'; +import { State, Store } from '@ngrx/store'; +import { AppState } from './store/state'; +import { + selectError, + selectHasConnectionSettings, + selectInterfaces, + selectMenuOpened, +} from './store/selectors'; +import { + fetchInterfaces, + fetchSystemConfig, + toggleMenu, + updateFocusNavigation, +} from './store/actions'; +import { appFeatureKey } from './store/reducers'; const DEVICES_LOGO_URL = '/assets/icons/devices.svg'; const REPORTS_LOGO_URL = '/assets/icons/reports.svg'; @@ -49,30 +56,37 @@ const CLOSE_URL = '/assets/icons/close.svg'; }) export class AppComponent implements OnInit { public readonly CalloutType = CalloutType; + public readonly StatusOfTestrun = StatusOfTestrun; + public readonly Routes = Routes; + devices$!: Observable; systemStatus$!: Observable; isTestrunStarted$!: Observable; - hasConnectionSetting$!: Observable; - interfaces: SystemInterfaces = {}; - isDevicesLoaded = false; + hasConnectionSetting$: Observable = this.store.select( + selectHasConnectionSettings + ); isStatusLoaded = false; isConnectionSettingsLoaded = false; - public readonly StatusOfTestrun = StatusOfTestrun; - public readonly Routes = Routes; private devicesLength = 0; private openedSettingFromToggleBtn = true; + isMenuOpen: Observable = this.store.select(selectMenuOpened); + interfaces: Observable = + this.store.select(selectInterfaces); + error$: Observable = this.store.select(selectError); + @ViewChild('settingsDrawer') public settingsDrawer!: MatDrawer; @ViewChild('toggleSettingsBtn') public toggleSettingsBtn!: HTMLButtonElement; @ViewChild('navigation') public navigation!: ElementRef; - @HostBinding('class.active-menu') isMenuOpen = false; constructor( private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer, private testRunService: TestRunService, private readonly loaderService: LoaderService, - private readonly state: StateService, - private route: Router + private route: Router, + private store: Store, + private state: State, + private readonly focusManagerService: FocusManagerService ) { this.testRunService.fetchDevices(); this.testRunService.getSystemStatus(); @@ -103,7 +117,6 @@ export class AppComponent implements OnInit { tap(result => { if (result !== null) { this.devicesLength = result.length; - this.isDevicesLoaded = true; } else { this.devicesLength = 0; } @@ -117,7 +130,7 @@ export class AppComponent implements OnInit { this.isTestrunStarted$ = this.testRunService.isTestrunStarted$; - this.hasConnectionSetting$ = this.testRunService.hasConnectionSetting$.pipe( + this.hasConnectionSetting$.pipe( tap(result => { if (result !== null) { this.isConnectionSettingsLoaded = true; @@ -125,6 +138,11 @@ export class AppComponent implements OnInit { }), shareReplay({ refCount: true, bufferSize: 1 }) ); + + this.getSystemInterfaces(); + + this.store.dispatch(fetchSystemConfig()); + //this.store.dispatch(fetchSystemConfigAndInterfaces()); } navigateToDeviceRepository(): void { @@ -142,9 +160,8 @@ export class AppComponent implements OnInit { if (this.devicesLength > 0) { this.toggleSettingsBtn.focus(); } // else device create window will be opened - if (!this.openedSettingFromToggleBtn) { - this.state.focusFirstElementInMain(); + this.focusManagerService.focusFirstElementInContainer(); } }); } @@ -158,44 +175,32 @@ export class AppComponent implements OnInit { this.getSystemInterfaces(); } - /** - * Indicates, if side menu should be focused on keyboard navigation after menu is opened - */ - focusNavigation = false; - public toggleMenu(event: MouseEvent) { event.stopPropagation(); - this.isMenuOpen = !this.isMenuOpen; - if (this.isMenuOpen) { - this.focusNavigation = true; // user will be navigated to side menu on tab - } + this.store.dispatch(toggleMenu()); } /** * When side menu is opened */ skipToNavigation(event: Event) { - if (this.focusNavigation) { + if (this.state.getValue()[appFeatureKey].appComponent.focusNavigation) { event.preventDefault(); // if not prevented, second element will be focused - this.navigation.nativeElement.firstChild.focus(); // focus first button on side - this.focusNavigation = false; // user will be navigated according to normal flow on tab + this.focusManagerService.focusFirstElementInContainer( + this.navigation.nativeElement + ); + this.store.dispatch(updateFocusNavigation({ focusNavigation: false })); // user will be navigated according to normal flow on tab } } async openGeneralSettings(openSettingFromToggleBtn: boolean) { this.openedSettingFromToggleBtn = openSettingFromToggleBtn; - this.getSystemInterfaces(); await this.settingsDrawer.open(); } private getSystemInterfaces(): void { - this.testRunService - .getSystemInterfaces() - .pipe(take(1)) - .subscribe(interfaces => { - this.interfaces = interfaces; - this.hideLoading(); - }); + this.store.dispatch(fetchInterfaces()); + this.hideLoading(); } private showLoading() { diff --git a/modules/ui/src/app/app.module.ts b/modules/ui/src/app/app.module.ts index e130fda29..ad6ce917a 100644 --- a/modules/ui/src/app/app.module.ts +++ b/modules/ui/src/app/app.module.ts @@ -26,7 +26,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { GeneralSettingsComponent } from './components/general-settings/general-settings.component'; +import { GeneralSettingsComponent } from './pages/settings/general-settings.component'; import { ReactiveFormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSnackBarModule } from '@angular/material/snack-bar'; @@ -38,6 +38,11 @@ import { SpinnerComponent } from './components/spinner/spinner.component'; import { BypassComponent } from './components/bypass/bypass.component'; import { VersionComponent } from './components/version/version.component'; import { CalloutComponent } from './components/callout/callout.component'; +import { StoreModule } from '@ngrx/store'; +import { appFeatureKey, rootReducer } from './store/reducers'; +import { EffectsModule } from '@ngrx/effects'; +import { AppEffects } from './store/effects'; +import { CdkTrapFocus } from '@angular/cdk/a11y'; @NgModule({ declarations: [AppComponent, GeneralSettingsComponent], @@ -61,6 +66,9 @@ import { CalloutComponent } from './components/callout/callout.component'; BypassComponent, VersionComponent, CalloutComponent, + StoreModule.forRoot({ [appFeatureKey]: rootReducer }), + EffectsModule.forRoot([AppEffects]), + CdkTrapFocus, ], providers: [ { diff --git a/modules/ui/src/app/components/bypass/bypass.component.spec.ts b/modules/ui/src/app/components/bypass/bypass.component.spec.ts index 93430f5c4..8ba531e7e 100644 --- a/modules/ui/src/app/components/bypass/bypass.component.spec.ts +++ b/modules/ui/src/app/components/bypass/bypass.component.spec.ts @@ -17,7 +17,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BypassComponent } from './bypass.component'; import { Component } from '@angular/core'; -import { StateService } from '../../services/state.service'; +import { FocusManagerService } from '../../services/focus-manager.service'; import SpyObj = jasmine.SpyObj; @Component({ @@ -31,14 +31,14 @@ class TestBypassComponent {} describe('BypassComponent', () => { let component: TestBypassComponent; let fixture: ComponentFixture; - let mockService: SpyObj; + let mockService: SpyObj; beforeEach(() => { - mockService = jasmine.createSpyObj(['focusFirstElementInMain']); + mockService = jasmine.createSpyObj(['focusFirstElementInContainer']); TestBed.configureTestingModule({ imports: [BypassComponent], declarations: [TestBypassComponent], - providers: [{ provide: StateService, useValue: mockService }], + providers: [{ provide: FocusManagerService, useValue: mockService }], }); fixture = TestBed.createComponent(TestBypassComponent); component = fixture.componentInstance; @@ -57,7 +57,7 @@ describe('BypassComponent', () => { button?.click(); - expect(mockService.focusFirstElementInMain).toHaveBeenCalled(); + expect(mockService.focusFirstElementInContainer).toHaveBeenCalled(); }); }); }); diff --git a/modules/ui/src/app/components/bypass/bypass.component.ts b/modules/ui/src/app/components/bypass/bypass.component.ts index ad29f4544..3a7d2819c 100644 --- a/modules/ui/src/app/components/bypass/bypass.component.ts +++ b/modules/ui/src/app/components/bypass/bypass.component.ts @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; -import { StateService } from '../../services/state.service'; +import { FocusManagerService } from '../../services/focus-manager.service'; @Component({ selector: 'app-bypass', @@ -26,9 +26,9 @@ import { StateService } from '../../services/state.service'; styleUrls: ['./bypass.component.scss'], }) export class BypassComponent { - constructor(private readonly state: StateService) {} + constructor(private readonly focusManagerService: FocusManagerService) {} skipToMainContent(event: Event) { event.preventDefault(); - this.state.focusFirstElementInMain(); + this.focusManagerService.focusFirstElementInContainer(); } } diff --git a/modules/ui/src/app/components/callout/callout.component.scss b/modules/ui/src/app/components/callout/callout.component.scss index d6b6f26c5..4f36d3487 100644 --- a/modules/ui/src/app/components/callout/callout.component.scss +++ b/modules/ui/src/app/components/callout/callout.component.scss @@ -21,10 +21,15 @@ width: 100%; } -:host:has(.callout-container.info) { +:host:has(.callout-container.info), +:host:has(.callout-container.error) { position: absolute; } +:host + ::ng-deep app-callout { + top: 60px; +} + .callout-container { display: flex; box-sizing: border-box; @@ -58,6 +63,15 @@ } } +.callout-container.error { + margin: 24px 32px; + background-color: $red-50; + + .callout-icon { + color: $red-700; + } +} + .callout-context { margin: 0; padding: 6px 0; diff --git a/modules/ui/src/app/components/device-item/device-item.component.scss b/modules/ui/src/app/components/device-item/device-item.component.scss index d636b4727..c591b1d5d 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.scss +++ b/modules/ui/src/app/components/device-item/device-item.component.scss @@ -131,7 +131,7 @@ $border-radius: 12px; font-style: normal; font-weight: 400; line-height: 20px; - max-width: 112px; + max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -146,6 +146,7 @@ $border-radius: 12px; font-size: 12px; padding-top: 2px; line-height: 20px; + max-width: 100%; } .button-start { diff --git a/modules/ui/src/app/components/device-item/device-item.component.spec.ts b/modules/ui/src/app/components/device-item/device-item.component.spec.ts index e94188040..b7dd90524 100644 --- a/modules/ui/src/app/components/device-item/device-item.component.spec.ts +++ b/modules/ui/src/app/components/device-item/device-item.component.spec.ts @@ -17,7 +17,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Device, DeviceView } from '../../model/device'; import { DeviceItemComponent } from './device-item.component'; -import { DeviceRepositoryModule } from '../../device-repository/device-repository.module'; +import { DeviceRepositoryModule } from '../../pages/devices/device-repository.module'; describe('DeviceItemComponent', () => { let component: DeviceItemComponent; diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.html b/modules/ui/src/app/components/general-settings/general-settings.component.html index c698e167f..e69de29bb 100644 --- a/modules/ui/src/app/components/general-settings/general-settings.component.html +++ b/modules/ui/src/app/components/general-settings/general-settings.component.html @@ -1,163 +0,0 @@ - -
-

Connection settings

- -
-
- diff --git a/modules/ui/src/app/model/callout-type.ts b/modules/ui/src/app/model/callout-type.ts index 49a2b73c6..250521227 100644 --- a/modules/ui/src/app/model/callout-type.ts +++ b/modules/ui/src/app/model/callout-type.ts @@ -16,4 +16,5 @@ export enum CalloutType { Info = 'info', Warning = 'warning_amber', + Error = 'error', } diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.html b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.html similarity index 100% rename from modules/ui/src/app/device-repository/device-form/device-form.component.html rename to modules/ui/src/app/pages/devices/components/device-form/device-form.component.html diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.scss b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss similarity index 97% rename from modules/ui/src/app/device-repository/device-form/device-form.component.scss rename to modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss index 3f16cfa84..96903ccc5 100644 --- a/modules/ui/src/app/device-repository/device-form/device-form.component.scss +++ b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; +@import 'src/theming/colors'; $device-form-max-width: 580px; $device-form-min-width: 285px; diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.spec.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts similarity index 97% rename from modules/ui/src/app/device-repository/device-form/device-form.component.spec.ts rename to modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts index 47f8fa423..592ebef70 100644 --- a/modules/ui/src/app/device-repository/device-form/device-form.component.spec.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.spec.ts @@ -21,7 +21,7 @@ import { } from '@angular/core/testing'; import { DeviceFormComponent, FormAction } from './device-form.component'; -import { TestRunService } from '../../services/test-run.service'; +import { TestRunService } from '../../../../services/test-run.service'; import { MatButtonModule } from '@angular/material/button'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -32,10 +32,10 @@ import { MatDialogRef, } from '@angular/material/dialog'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Device } from '../../model/device'; +import { Device } from '../../../../model/device'; import { of, throwError } from 'rxjs'; -import { DeviceTestsComponent } from '../../components/device-tests/device-tests.component'; -import { SpinnerComponent } from '../../components/spinner/spinner.component'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; describe('DeviceFormComponent', () => { diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.ts b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts similarity index 95% rename from modules/ui/src/app/device-repository/device-form/device-form.component.ts rename to modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts index f33271b95..497a9067b 100644 --- a/modules/ui/src/app/device-repository/device-form/device-form.component.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device-form.component.ts @@ -22,12 +22,12 @@ import { Validators, } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Device, TestModule } from '../../model/device'; -import { TestRunService } from '../../services/test-run.service'; +import { Device, TestModule } from '../../../../model/device'; +import { TestRunService } from '../../../../services/test-run.service'; import { DeviceValidators } from './device.validators'; import { catchError, of, Subject, takeUntil } from 'rxjs'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { EscapableDialogComponent } from '../../components/escapable-dialog/escapable-dialog.component'; +import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; import { Observable } from 'rxjs/internal/Observable'; const MAC_ADDRESS_PATTERN = diff --git a/modules/ui/src/app/device-repository/device-form/device.validators.ts b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts similarity index 93% rename from modules/ui/src/app/device-repository/device-form/device.validators.ts rename to modules/ui/src/app/pages/devices/components/device-form/device.validators.ts index dc6230502..288c05daa 100644 --- a/modules/ui/src/app/device-repository/device-form/device.validators.ts +++ b/modules/ui/src/app/pages/devices/components/device-form/device.validators.ts @@ -15,8 +15,8 @@ */ import { Injectable } from '@angular/core'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; -import { TestRunService } from '../../services/test-run.service'; -import { Device } from '../../model/device'; +import { TestRunService } from '../../../../services/test-run.service'; +import { Device } from '../../../../model/device'; @Injectable({ providedIn: 'root' }) diff --git a/modules/ui/src/app/device-repository/device-repository-routing.module.ts b/modules/ui/src/app/pages/devices/device-repository-routing.module.ts similarity index 100% rename from modules/ui/src/app/device-repository/device-repository-routing.module.ts rename to modules/ui/src/app/pages/devices/device-repository-routing.module.ts diff --git a/modules/ui/src/app/device-repository/device-repository.component.html b/modules/ui/src/app/pages/devices/device-repository.component.html similarity index 100% rename from modules/ui/src/app/device-repository/device-repository.component.html rename to modules/ui/src/app/pages/devices/device-repository.component.html diff --git a/modules/ui/src/app/device-repository/device-repository.component.scss b/modules/ui/src/app/pages/devices/device-repository.component.scss similarity index 94% rename from modules/ui/src/app/device-repository/device-repository.component.scss rename to modules/ui/src/app/pages/devices/device-repository.component.scss index d83737755..d15f6bad5 100644 --- a/modules/ui/src/app/device-repository/device-repository.component.scss +++ b/modules/ui/src/app/pages/devices/device-repository.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../theming/colors'; -@import '../../theming/variables'; +@import 'src/theming/colors'; +@import 'src/theming/variables'; :host { overflow: hidden; diff --git a/modules/ui/src/app/device-repository/device-repository.component.spec.ts b/modules/ui/src/app/pages/devices/device-repository.component.spec.ts similarity index 94% rename from modules/ui/src/app/device-repository/device-repository.component.spec.ts rename to modules/ui/src/app/pages/devices/device-repository.component.spec.ts index 3bcc131f3..1f53aeaf5 100644 --- a/modules/ui/src/app/device-repository/device-repository.component.spec.ts +++ b/modules/ui/src/app/pages/devices/device-repository.component.spec.ts @@ -15,8 +15,8 @@ */ import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; -import { Device } from '../model/device'; -import { TestRunService } from '../services/test-run.service'; +import { Device } from '../../model/device'; +import { TestRunService } from '../../services/test-run.service'; import { DeviceRepositoryComponent } from './device-repository.component'; import { DeviceRepositoryModule } from './device-repository.module'; @@ -24,13 +24,13 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { DeviceFormComponent, FormAction, -} from './device-form/device-form.component'; +} from './components/device-form/device-form.component'; import { MatDialogRef } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { device } from '../mocks/device.mock'; -import { DeleteFormComponent } from '../components/delete-form/delete-form.component'; +import { device } from '../../mocks/device.mock'; +import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; import SpyObj = jasmine.SpyObj; -import { StateService } from '../services/state.service'; +import { FocusManagerService } from '../../services/focus-manager.service'; describe('DeviceRepositoryComponent', () => { let component: DeviceRepositoryComponent; @@ -38,10 +38,8 @@ describe('DeviceRepositoryComponent', () => { let compiled: HTMLElement; let mockService: SpyObj; - const stateServiceMock: jasmine.SpyObj = jasmine.createSpyObj( - 'stateServiceMock', - ['focusFirstElementInMain'] - ); + const stateServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('stateServiceMock', ['focusFirstElementInContainer']); beforeEach(() => { mockService = jasmine.createSpyObj([ @@ -75,7 +73,7 @@ describe('DeviceRepositoryComponent', () => { imports: [DeviceRepositoryModule, BrowserAnimationsModule], providers: [ { provide: TestRunService, useValue: mockService }, - { provide: StateService, useValue: stateServiceMock }, + { provide: FocusManagerService, useValue: stateServiceMock }, ], declarations: [DeviceRepositoryComponent], }); diff --git a/modules/ui/src/app/device-repository/device-repository.component.ts b/modules/ui/src/app/pages/devices/device-repository.component.ts similarity index 89% rename from modules/ui/src/app/device-repository/device-repository.component.ts rename to modules/ui/src/app/pages/devices/device-repository.component.ts index 5e513cd6e..e871f6b94 100644 --- a/modules/ui/src/app/device-repository/device-repository.component.ts +++ b/modules/ui/src/app/pages/devices/device-repository.component.ts @@ -22,21 +22,21 @@ import { } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Observable } from 'rxjs/internal/Observable'; -import { Device, DeviceView } from '../model/device'; -import { TestRunService } from '../services/test-run.service'; +import { Device, DeviceView } from '../../model/device'; +import { TestRunService } from '../../services/test-run.service'; import { DeviceFormComponent, FormAction, FormResponse, -} from './device-form/device-form.component'; +} from './components/device-form/device-form.component'; import { Subject, takeUntil } from 'rxjs'; -import { DeleteFormComponent } from '../components/delete-form/delete-form.component'; +import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; import { combineLatest } from 'rxjs/internal/observable/combineLatest'; -import { ProgressInitiateFormComponent } from '../progress/progress-initiate-form/progress-initiate-form.component'; -import { Routes } from '../model/routes'; +import { FocusManagerService } from '../../services/focus-manager.service'; +import { Routes } from '../../model/routes'; import { Router } from '@angular/router'; -import { StateService } from '../services/state.service'; import { timer } from 'rxjs/internal/observable/timer'; +import { ProgressInitiateFormComponent } from '../testrun/components/progress-initiate-form/progress-initiate-form.component'; @Component({ selector: 'app-device-repository', @@ -51,7 +51,7 @@ export class DeviceRepositoryComponent implements OnInit, OnDestroy { constructor( private testRunService: TestRunService, - private readonly state: StateService, + private readonly focusManagerService: FocusManagerService, public dialog: MatDialog, private element: ElementRef, private readonly changeDetectorRef: ChangeDetectorRef, @@ -126,7 +126,7 @@ export class DeviceRepositoryComponent implements OnInit, OnDestroy { timer(10) .pipe(takeUntil(this.destroy$)) .subscribe(() => { - this.state.focusFirstElementInMain(); + this.focusManagerService.focusFirstElementInContainer(); }); } if ( diff --git a/modules/ui/src/app/device-repository/device-repository.module.ts b/modules/ui/src/app/pages/devices/device-repository.module.ts similarity index 82% rename from modules/ui/src/app/device-repository/device-repository.module.ts rename to modules/ui/src/app/pages/devices/device-repository.module.ts index bc7935875..cadfc216a 100644 --- a/modules/ui/src/app/device-repository/device-repository.module.ts +++ b/modules/ui/src/app/pages/devices/device-repository.module.ts @@ -24,14 +24,14 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { DeviceFormComponent } from './device-form/device-form.component'; +import { DeviceFormComponent } from './components/device-form/device-form.component'; import { DeviceRepositoryRoutingModule } from './device-repository-routing.module'; import { DeviceRepositoryComponent } from './device-repository.component'; -import { DeviceItemComponent } from '../components/device-item/device-item.component'; -import { DeviceTestsComponent } from '../components/device-tests/device-tests.component'; -import { SpinnerComponent } from '../components/spinner/spinner.component'; -import { DeleteFormComponent } from '../components/delete-form/delete-form.component'; +import { DeviceItemComponent } from '../../components/device-item/device-item.component'; +import { DeviceTestsComponent } from '../../components/device-tests/device-tests.component'; +import { SpinnerComponent } from '../../components/spinner/spinner.component'; +import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; @NgModule({ diff --git a/modules/ui/src/app/components/delete-report/delete-report.component.html b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.html similarity index 100% rename from modules/ui/src/app/components/delete-report/delete-report.component.html rename to modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.html diff --git a/modules/ui/src/app/components/delete-report/delete-report.component.scss b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.scss similarity index 100% rename from modules/ui/src/app/components/delete-report/delete-report.component.scss rename to modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.scss diff --git a/modules/ui/src/app/components/delete-report/delete-report.component.spec.ts b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts similarity index 93% rename from modules/ui/src/app/components/delete-report/delete-report.component.spec.ts rename to modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts index 11e9c2a2e..846d14c0f 100644 --- a/modules/ui/src/app/components/delete-report/delete-report.component.spec.ts +++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.spec.ts @@ -18,10 +18,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeleteReportComponent } from './delete-report.component'; import { of } from 'rxjs'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { DeleteFormComponent } from '../delete-form/delete-form.component'; -import { TestRunService } from '../../services/test-run.service'; +import { DeleteFormComponent } from '../../../../components/delete-form/delete-form.component'; +import { TestRunService } from '../../../../services/test-run.service'; import SpyObj = jasmine.SpyObj; -import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../mocks/progress.mock'; +import { MOCK_PROGRESS_DATA_COMPLIANT } from '../../../../mocks/progress.mock'; describe('DeleteReportComponent', () => { let compiled: HTMLElement; diff --git a/modules/ui/src/app/components/delete-report/delete-report.component.ts b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts similarity index 90% rename from modules/ui/src/app/components/delete-report/delete-report.component.ts rename to modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts index 4f76b1859..3f5dd91bc 100644 --- a/modules/ui/src/app/components/delete-report/delete-report.component.ts +++ b/modules/ui/src/app/pages/reports/components/delete-report/delete-report.component.ts @@ -22,10 +22,10 @@ import { } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; -import { ReportActionComponent } from '../report-action/report-action.component'; -import { TestRunService } from '../../services/test-run.service'; +import { ReportActionComponent } from '../../../../components/report-action/report-action.component'; +import { TestRunService } from '../../../../services/test-run.service'; import { MatDialog } from '@angular/material/dialog'; -import { DeleteFormComponent } from '../delete-form/delete-form.component'; +import { DeleteFormComponent } from '../../../../components/delete-form/delete-form.component'; import { takeUntil } from 'rxjs/internal/operators/takeUntil'; import { Subject } from 'rxjs'; diff --git a/modules/ui/src/app/components/filter-chips/filter-chips.component.html b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.html similarity index 100% rename from modules/ui/src/app/components/filter-chips/filter-chips.component.html rename to modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.html diff --git a/modules/ui/src/app/components/filter-chips/filter-chips.component.scss b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.scss similarity index 97% rename from modules/ui/src/app/components/filter-chips/filter-chips.component.scss rename to modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.scss index 5dc1c5ede..ec7ef97e0 100644 --- a/modules/ui/src/app/components/filter-chips/filter-chips.component.scss +++ b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; +@import 'src/theming/colors'; :host { display: flex; diff --git a/modules/ui/src/app/components/filter-chips/filter-chips.component.spec.ts b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts similarity index 100% rename from modules/ui/src/app/components/filter-chips/filter-chips.component.spec.ts rename to modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.spec.ts diff --git a/modules/ui/src/app/components/filter-chips/filter-chips.component.ts b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.ts similarity index 96% rename from modules/ui/src/app/components/filter-chips/filter-chips.component.ts rename to modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.ts index 3a6d0c9e3..45e4cce62 100644 --- a/modules/ui/src/app/components/filter-chips/filter-chips.component.ts +++ b/modules/ui/src/app/pages/reports/components/filter-chips/filter-chips.component.ts @@ -17,7 +17,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { CommonModule, KeyValuePipe } from '@angular/common'; -import { DateRange, FilterName, Filters } from '../../model/filters'; +import { DateRange, FilterName, Filters } from '../../../../model/filters'; @Component({ selector: 'app-filter-chips', diff --git a/modules/ui/src/app/components/filter-dialog/filter-dialog.component.html b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.html similarity index 100% rename from modules/ui/src/app/components/filter-dialog/filter-dialog.component.html rename to modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.html diff --git a/modules/ui/src/app/components/filter-dialog/filter-dialog.component.scss b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.scss similarity index 93% rename from modules/ui/src/app/components/filter-dialog/filter-dialog.component.scss rename to modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.scss index e8f24fcc8..3dfdd2704 100644 --- a/modules/ui/src/app/components/filter-dialog/filter-dialog.component.scss +++ b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@use '@angular/material' as mat; -@import '../../../theming/colors'; +@use 'node_modules/@angular/material/index' as mat; +@import 'src/theming/colors'; .filter-dialog-content { display: flex; diff --git a/modules/ui/src/app/components/filter-dialog/filter-dialog.component.spec.ts b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.spec.ts similarity index 98% rename from modules/ui/src/app/components/filter-dialog/filter-dialog.component.spec.ts rename to modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.spec.ts index 4ecc15e52..eabd7125d 100644 --- a/modules/ui/src/app/components/filter-dialog/filter-dialog.component.spec.ts +++ b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.spec.ts @@ -33,7 +33,7 @@ import { MatDatepickerModule, } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; -import { DateRange, FilterName } from '../../model/filters'; +import { DateRange, FilterName } from '../../../../model/filters'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of } from 'rxjs'; diff --git a/modules/ui/src/app/components/filter-dialog/filter-dialog.component.ts b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.ts similarity index 96% rename from modules/ui/src/app/components/filter-dialog/filter-dialog.component.ts rename to modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.ts index d8ed991ae..d57b248e2 100644 --- a/modules/ui/src/app/components/filter-dialog/filter-dialog.component.ts +++ b/modules/ui/src/app/pages/reports/components/filter-dialog/filter-dialog.component.ts @@ -51,8 +51,11 @@ import { MatDatepickerModule, } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; -import { FilterName, DateRange as LocalDateRange } from '../../model/filters'; -import { EscapableDialogComponent } from '../escapable-dialog/escapable-dialog.component'; +import { + FilterName, + DateRange as LocalDateRange, +} from '../../../../model/filters'; +import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; interface DialogData { trigger: ElementRef; diff --git a/modules/ui/src/app/history/history-routing.module.ts b/modules/ui/src/app/pages/reports/history-routing.module.ts similarity index 100% rename from modules/ui/src/app/history/history-routing.module.ts rename to modules/ui/src/app/pages/reports/history-routing.module.ts diff --git a/modules/ui/src/app/history/history.component.html b/modules/ui/src/app/pages/reports/history.component.html similarity index 100% rename from modules/ui/src/app/history/history.component.html rename to modules/ui/src/app/pages/reports/history.component.html diff --git a/modules/ui/src/app/history/history.component.scss b/modules/ui/src/app/pages/reports/history.component.scss similarity index 94% rename from modules/ui/src/app/history/history.component.scss rename to modules/ui/src/app/pages/reports/history.component.scss index af0f8ffed..c0d2375e7 100644 --- a/modules/ui/src/app/history/history.component.scss +++ b/modules/ui/src/app/pages/reports/history.component.scss @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@use '@angular/material' as mat; -@import '../../theming/colors'; -@import '../../theming/variables'; +@use 'node_modules/@angular/material/index' as mat; +@import 'src/theming/colors'; +@import 'src/theming/variables'; :host { overflow: hidden; @@ -142,7 +142,7 @@ .results-content-empty-message-img { width: 293px; height: 154px; - background-image: url(/assets/icons/desktop.svg); + background-image: url(/src/assets/icons/desktop.svg); } .results-content-filter-empty .results-content-empty-message-main { diff --git a/modules/ui/src/app/history/history.component.spec.ts b/modules/ui/src/app/pages/reports/history.component.spec.ts similarity index 97% rename from modules/ui/src/app/history/history.component.spec.ts rename to modules/ui/src/app/pages/reports/history.component.spec.ts index 87ee2ab21..ebf4f7fed 100644 --- a/modules/ui/src/app/history/history.component.spec.ts +++ b/modules/ui/src/app/pages/reports/history.component.spec.ts @@ -16,16 +16,16 @@ import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { HistoryComponent } from './history.component'; -import { TestRunService } from '../services/test-run.service'; +import { TestRunService } from '../../services/test-run.service'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HistoryModule } from './history.module'; import { of } from 'rxjs'; -import { TestrunStatus } from '../model/testrun-status'; +import { TestrunStatus } from '../../model/testrun-status'; import { LiveAnnouncer } from '@angular/cdk/a11y'; import { MatDialogRef } from '@angular/material/dialog'; -import { FilterDialogComponent } from '../components/filter-dialog/filter-dialog.component'; +import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; import { ElementRef } from '@angular/core'; -import { FilterName, ReportFilters } from '../model/filters'; +import { FilterName, ReportFilters } from '../../model/filters'; import SpyObj = jasmine.SpyObj; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { MatSort } from '@angular/material/sort'; diff --git a/modules/ui/src/app/history/history.component.ts b/modules/ui/src/app/pages/reports/history.component.ts similarity index 97% rename from modules/ui/src/app/history/history.component.ts rename to modules/ui/src/app/pages/reports/history.component.ts index bb4b54b5e..780373ea9 100644 --- a/modules/ui/src/app/history/history.component.ts +++ b/modules/ui/src/app/pages/reports/history.component.ts @@ -21,20 +21,20 @@ import { ViewChild, } from '@angular/core'; import { LiveAnnouncer } from '@angular/cdk/a11y'; -import { TestRunService } from '../services/test-run.service'; +import { TestRunService } from '../../services/test-run.service'; import { HistoryTestrun, StatusResultClassName, TestrunStatus, -} from '../model/testrun-status'; +} from '../../model/testrun-status'; import { DatePipe } from '@angular/common'; import { MatSort, Sort } from '@angular/material/sort'; import { Subject, takeUntil } from 'rxjs'; import { MatRow, MatTableDataSource } from '@angular/material/table'; -import { FilterDialogComponent } from '../components/filter-dialog/filter-dialog.component'; +import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; import { MatDialog } from '@angular/material/dialog'; import { tap } from 'rxjs/internal/operators/tap'; -import { DateRange, FilterName, Filters } from '../model/filters'; +import { DateRange, FilterName, Filters } from '../../model/filters'; @Component({ selector: 'app-history', diff --git a/modules/ui/src/app/history/history.module.ts b/modules/ui/src/app/pages/reports/history.module.ts similarity index 78% rename from modules/ui/src/app/history/history.module.ts rename to modules/ui/src/app/pages/reports/history.module.ts index 288f4e048..59ac0f0d3 100644 --- a/modules/ui/src/app/history/history.module.ts +++ b/modules/ui/src/app/pages/reports/history.module.ts @@ -20,11 +20,11 @@ import { HistoryRoutingModule } from './history-routing.module'; import { MatTableModule } from '@angular/material/table'; import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { DownloadReportComponent } from '../components/download-report/download-report.component'; +import { DownloadReportComponent } from '../../components/download-report/download-report.component'; import { MatSortModule } from '@angular/material/sort'; -import { FilterDialogComponent } from '../components/filter-dialog/filter-dialog.component'; -import { FilterChipsComponent } from '../components/filter-chips/filter-chips.component'; -import { DeleteReportComponent } from '../components/delete-report/delete-report.component'; +import { FilterDialogComponent } from './components/filter-dialog/filter-dialog.component'; +import { FilterChipsComponent } from './components/filter-chips/filter-chips.component'; +import { DeleteReportComponent } from './components/delete-report/delete-report.component'; @NgModule({ declarations: [HistoryComponent], diff --git a/modules/ui/src/app/pages/settings/general-settings.component.html b/modules/ui/src/app/pages/settings/general-settings.component.html new file mode 100644 index 000000000..1ed1aff22 --- /dev/null +++ b/modules/ui/src/app/pages/settings/general-settings.component.html @@ -0,0 +1,163 @@ + +
+

Connection settings

+ +
+
+
+ + + Warning! Testrun requires two ports to operate correctly. + + +

+ Choose one of the options where you’ll connect IoT device for testing +

+ + Device connection port + + + +

+ {{ deviceControl.value.key }} +

+

+ {{ deviceControl.value.value }} +

+
+
+ +

{{ interface.key }}

+

{{ interface.value }}

+
+
+
+ +

+ Choose one of the following ports on your Laptop or Desktop +

+ + Internet connection port + + + +

+ {{ internetControl.value.key }} +

+

+ {{ internetControl.value.value }} +

+
+
+ +

{{ defaultInternetOption.key }}

+

{{ defaultInternetOption.value }}

+
+ +

{{ interface.key }}

+

{{ interface.value }}

+
+
+
+
+

+ If a port is missing from this list, you can + + Refresh + + the Connection settings +

+ + Both interfaces must have different values + + +
+ + + Warning! No ports is detected. + + +
+ diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.scss b/modules/ui/src/app/pages/settings/general-settings.component.scss similarity index 100% rename from modules/ui/src/app/components/general-settings/general-settings.component.scss rename to modules/ui/src/app/pages/settings/general-settings.component.scss diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts b/modules/ui/src/app/pages/settings/general-settings.component.spec.ts similarity index 81% rename from modules/ui/src/app/components/general-settings/general-settings.component.spec.ts rename to modules/ui/src/app/pages/settings/general-settings.component.spec.ts index e9b82ddca..1cb80f8cf 100644 --- a/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts +++ b/modules/ui/src/app/pages/settings/general-settings.component.spec.ts @@ -16,10 +16,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GeneralSettingsComponent } from './general-settings.component'; -import { - SystemInterfaces, - TestRunService, -} from '../../services/test-run.service'; +import { SystemInterfaces } from '../../services/test-run.service'; import { of } from 'rxjs'; import { SystemConfig } from '../../model/setting'; import { MatRadioModule } from '@angular/material/radio'; @@ -33,13 +30,11 @@ import SpyObj = jasmine.SpyObj; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -const MOCK_SYSTEM_CONFIG_EMPTY: SystemConfig = { - network: { - device_intf: '', - internet_intf: '', - }, -}; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { AppEffects } from '../../store/effects'; +import { selectSystemConfig } from '../../store/selectors'; +import { AppState } from '../../store/state'; +import { createSystemConfig } from '../../store/actions'; const MOCK_SYSTEM_CONFIG_WITH_DATA: SystemConfig = { network: { @@ -56,42 +51,28 @@ const MOCK_INTERFACES: SystemInterfaces = { describe('GeneralSettingsComponent', () => { let component: GeneralSettingsComponent; let fixture: ComponentFixture; - let testRunServiceMock: SpyObj; let mockLiveAnnouncer: SpyObj; let compiled: HTMLElement; + let mockEffects: SpyObj; + let store: MockStore; beforeEach(async () => { - testRunServiceMock = jasmine.createSpyObj([ - 'getSystemInterfaces', - 'getSystemConfig', - 'setSystemConfig', - 'createSystemConfig', - 'hasConnectionSetting$', - 'setHasConnectionSetting', - 'systemConfig$', - ]); - testRunServiceMock.getSystemInterfaces.and.returnValue(of({})); - testRunServiceMock.getSystemConfig.and.returnValue( - of(MOCK_SYSTEM_CONFIG_EMPTY) - ); - testRunServiceMock.createSystemConfig.and.returnValue( - of(MOCK_SYSTEM_CONFIG_WITH_DATA) - ); - testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_EMPTY); - testRunServiceMock.hasConnectionSetting$ = of(true); - mockLiveAnnouncer = jasmine.createSpyObj(['announce']); + mockEffects = jasmine.createSpyObj(['onCreateSystemConfigSuccess$']); + + // @ts-expect-error mock empty value in test + mockEffects.onCreateSystemConfigSuccess$ = of({}); await TestBed.configureTestingModule({ declarations: [ GeneralSettingsComponent, - MatIcon, FakeSpinnerComponent, FakeCalloutComponent, ], providers: [ - { provide: TestRunService, useValue: testRunServiceMock }, { provide: LiveAnnouncer, useValue: mockLiveAnnouncer }, + { provide: AppEffects, useValue: mockEffects }, + provideMockStore(), ], imports: [ BrowserAnimationsModule, @@ -100,6 +81,7 @@ describe('GeneralSettingsComponent', () => { MatRadioModule, ReactiveFormsModule, MatIconTestingModule, + MatIcon, MatInputModule, MatSelectModule, ], @@ -109,6 +91,11 @@ describe('GeneralSettingsComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); compiled = fixture.nativeElement as HTMLElement; + store = TestBed.inject(MockStore); + + store.overrideSelector(selectSystemConfig, MOCK_SYSTEM_CONFIG_WITH_DATA); + component.ngOnInit(); + spyOn(store, 'dispatch').and.callFake(() => {}); }); it('should create', () => { @@ -116,9 +103,6 @@ describe('GeneralSettingsComponent', () => { }); it('should set default values to form if systemConfig data', () => { - testRunServiceMock.getSystemConfig.and.returnValue( - of(MOCK_SYSTEM_CONFIG_WITH_DATA) - ); component.interfaces = { mockDeviceKey: 'mockDeviceValue' }; const expectedDevice = { key: 'mockDeviceKey', value: 'mockDeviceValue' }; @@ -141,8 +125,9 @@ describe('GeneralSettingsComponent', () => { describe('#closeSetting', () => { beforeEach(() => { - testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); + store.overrideSelector(selectSystemConfig, MOCK_SYSTEM_CONFIG_WITH_DATA); component.interfaces = MOCK_INTERFACES; + component.ngOnInit(); }); it('should emit closeSettingEvent', () => { @@ -190,7 +175,8 @@ describe('GeneralSettingsComponent', () => { describe('#saveSetting', () => { beforeEach(() => { - testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); + store.overrideSelector(selectSystemConfig, MOCK_SYSTEM_CONFIG_WITH_DATA); + component.ngOnInit(); }); it('should have form error if form has the same value', () => { @@ -222,8 +208,8 @@ describe('GeneralSettingsComponent', () => { component.saveSetting(); expect(component.settingForm.invalid).toBeFalse(); - expect(testRunServiceMock.createSystemConfig).toHaveBeenCalledWith( - expectedResult + expect(store.dispatch).toHaveBeenCalledWith( + createSystemConfig({ data: expectedResult }) ); }); }); @@ -249,13 +235,9 @@ describe('GeneralSettingsComponent', () => { }); }); - describe('with intefaces lenght less then two', () => { + describe('with interfaces length less than one', () => { beforeEach(() => { - component.interfaces = { mockDeviceValue: 'mockDeviceValue' }; - testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); - testRunServiceMock.getSystemConfig.and.returnValue( - of(MOCK_SYSTEM_CONFIG_WITH_DATA) - ); + component.interfaces = {}; fixture.detectChanges(); }); @@ -288,10 +270,6 @@ describe('GeneralSettingsComponent', () => { mockDeviceValue: 'mockDeviceValue', mockInterfaceValue: 'mockInterfaceValue', }; - testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); - testRunServiceMock.getSystemConfig.and.returnValue( - of(MOCK_SYSTEM_CONFIG_WITH_DATA) - ); fixture.detectChanges(); }); diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.ts b/modules/ui/src/app/pages/settings/general-settings.component.ts similarity index 67% rename from modules/ui/src/app/components/general-settings/general-settings.component.ts rename to modules/ui/src/app/pages/settings/general-settings.component.ts index c26c535da..7b5e90a84 100644 --- a/modules/ui/src/app/components/general-settings/general-settings.component.ts +++ b/modules/ui/src/app/pages/settings/general-settings.component.ts @@ -23,33 +23,27 @@ import { } from '@angular/core'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Subject, takeUntil, tap } from 'rxjs'; -import { - SystemInterfaces, - TestRunService, -} from '../../services/test-run.service'; +import { SystemInterfaces } from '../../services/test-run.service'; import { OnlyDifferentValuesValidator } from './only-different-values.validator'; import { CalloutType } from '../../model/callout-type'; -import { Observable } from 'rxjs/internal/Observable'; -import { shareReplay } from 'rxjs/internal/operators/shareReplay'; -import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { CdkTrapFocus, LiveAnnouncer } from '@angular/cdk/a11y'; import { EventType } from '../../model/event-type'; -import { SettingOption } from '../../model/setting'; +import { SettingOption, SystemConfig } from '../../model/setting'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../store/state'; +import { selectSystemConfig } from '../../store/selectors'; +import { createSystemConfig } from '../../store/actions'; +import { AppEffects } from '../../store/effects'; @Component({ selector: 'app-general-settings', templateUrl: './general-settings.component.html', styleUrls: ['./general-settings.component.scss'], + hostDirectives: [CdkTrapFocus], }) export class GeneralSettingsComponent implements OnInit, OnDestroy { - private _interfaces: SystemInterfaces = {}; - @Input() - get interfaces(): SystemInterfaces { - return this._interfaces; - } - set interfaces(value: SystemInterfaces) { - this._interfaces = value; - this.setSystemSetting(); - } + @Input() interfaces: SystemInterfaces = {}; + @Input() hasConnectionSettings = false; @Output() closeSettingEvent = new EventEmitter(); @Output() reloadInterfacesEvent = new EventEmitter(); public readonly CalloutType = CalloutType; @@ -60,9 +54,8 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { key: '', value: 'Not specified', }; - hasConnectionSetting$!: Observable; private destroy$: Subject = new Subject(); - + private systemConfig: SystemConfig = {}; get deviceControl(): FormControl { return this.settingForm.get('device_intf') as FormControl; } @@ -79,38 +72,53 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { return this.settingForm.hasError('hasSameValues'); } - get isLessThanTwoInterfaces(): boolean { - return Object.keys(this.interfaces).length < 2; + get isLessThanOneInterfaces(): boolean { + return Object.keys(this.interfaces).length < 1; } constructor( - private readonly testRunService: TestRunService, private readonly fb: FormBuilder, private liveAnnouncer: LiveAnnouncer, - private readonly onlyDifferentValuesValidator: OnlyDifferentValuesValidator + private readonly onlyDifferentValuesValidator: OnlyDifferentValuesValidator, + private store: Store, + private effects: AppEffects ) {} ngOnInit() { - this.hasConnectionSetting$ = this.testRunService.hasConnectionSetting$.pipe( - shareReplay({ refCount: true, bufferSize: 1 }) - ); - this.createSettingForm(); - this.setSettingView(); this.cleanFormErrorMessage(); + + this.store + .select(selectSystemConfig) + .pipe(takeUntil(this.destroy$)) + .subscribe(config => { + this.systemConfig = config; + this.setDefaultFormValues( + this.systemConfig?.network?.device_intf, + this.systemConfig?.network?.internet_intf + ); + }); + + this.effects.onCreateSystemConfigSuccess$ + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.closeSetting(EventType.Save); + }); } reloadSetting(): void { this.reloadInterfacesEvent.emit(); } - closeSetting(message: string): void { this.resetForm(); this.closeSettingEvent.emit(); this.liveAnnouncer.announce( `The ${message} finished. The connection setting panel is closed.` ); - this.setSystemSetting(); + this.setDefaultFormValues( + this.systemConfig?.network?.device_intf, + this.systemConfig?.network?.internet_intf + ); } saveSetting(): void { @@ -135,26 +143,6 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { )); } - private setSettingView(): void { - this.testRunService - .getSystemConfig() - .pipe(takeUntil(this.destroy$)) - .subscribe(config => { - if (config?.network) { - const { device_intf, internet_intf } = config.network; - if (device_intf && internet_intf) { - this.testRunService.setHasConnectionSetting(true); - } else { - this.testRunService.setHasConnectionSetting(false); - } - this.setDefaultFormValues(device_intf, internet_intf); - } else { - this.testRunService.setHasConnectionSetting(false); - } - this.testRunService.setSystemConfig(config); - }); - } - compare(c1: SettingOption, c2: SettingOption): boolean { return c1 && c2 && c1.key === c2.key && c1.value === c2.value; } @@ -168,8 +156,8 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { this.deviceControl.setValue(deviceData); } if (internet && this.interfaces[internet]) { - const interneData = this.transformValueToObj(internet); - this.internetControl.setValue(interneData); + const internetData = this.transformValueToObj(internet); + this.internetControl.setValue(internetData); } else { this.internetControl.setValue(this.defaultInternetOption); } @@ -193,32 +181,14 @@ export class GeneralSettingsComponent implements OnInit, OnDestroy { private createSystemConfig(): void { const { device_intf, internet_intf } = this.settingForm.value; - const data = { + const data: SystemConfig = { network: { device_intf: device_intf.key, internet_intf: internet_intf.key, }, }; - this.testRunService - .createSystemConfig(data) - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.closeSetting(EventType.Save); - this.testRunService.setSystemConfig(data); - this.testRunService.setHasConnectionSetting(true); - }); - } - - private setSystemSetting(): void { - this.testRunService.systemConfig$ - .pipe(takeUntil(this.destroy$)) - .subscribe(config => { - if (config?.network) { - const { device_intf, internet_intf } = config.network; - this.setDefaultFormValues(device_intf, internet_intf); - } - }); + this.store.dispatch(createSystemConfig({ data })); } private resetForm(): void { diff --git a/modules/ui/src/app/components/general-settings/only-different-values.validator.ts b/modules/ui/src/app/pages/settings/only-different-values.validator.ts similarity index 100% rename from modules/ui/src/app/components/general-settings/only-different-values.validator.ts rename to modules/ui/src/app/pages/settings/only-different-values.validator.ts diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html b/modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.html similarity index 100% rename from modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html rename to modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.html diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.scss b/modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.scss similarity index 94% rename from modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.scss rename to modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.scss index a59794a9d..082599567 100644 --- a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.scss +++ b/modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; -@import '../../../theming/variables'; +@import 'src/theming/colors'; +@import 'src/theming/variables'; :host { display: grid; diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.spec.ts b/modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.spec.ts similarity index 94% rename from modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.spec.ts rename to modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.spec.ts index b3f66b3cf..58b928605 100644 --- a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.spec.ts @@ -21,18 +21,18 @@ import { MatDialogModule, MatDialogRef, } from '@angular/material/dialog'; -import { TestRunService } from '../../services/test-run.service'; +import { TestRunService } from '../../../../services/test-run.service'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { Device } from '../../model/device'; -import { DeviceItemComponent } from '../../components/device-item/device-item.component'; +import { Device } from '../../../../model/device'; +import { DeviceItemComponent } from '../../../../components/device-item/device-item.component'; import { ReactiveFormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DeviceTestsComponent } from '../../components/device-tests/device-tests.component'; -import { device } from '../../mocks/device.mock'; +import { DeviceTestsComponent } from '../../../../components/device-tests/device-tests.component'; +import { device } from '../../../../mocks/device.mock'; import { of } from 'rxjs'; -import { MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE } from '../../mocks/progress.mock'; -import { SpinnerComponent } from '../../components/spinner/spinner.component'; +import { MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE } from '../../../../mocks/progress.mock'; +import { SpinnerComponent } from '../../../../components/spinner/spinner.component'; describe('ProgressInitiateFormComponent', () => { let component: ProgressInitiateFormComponent; diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.ts b/modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.ts similarity index 91% rename from modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.ts rename to modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.ts index d124ad7fb..29c46c683 100644 --- a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.ts +++ b/modules/ui/src/app/pages/testrun/components/progress-initiate-form/progress-initiate-form.component.ts @@ -23,17 +23,17 @@ import { ViewChild, } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { TestRunService } from '../../services/test-run.service'; +import { TestRunService } from '../../../../services/test-run.service'; import { Observable } from 'rxjs/internal/Observable'; -import { Device, TestModule, DeviceView } from '../../model/device'; +import { Device, TestModule, DeviceView } from '../../../../model/device'; import { AbstractControl, FormArray, FormBuilder, FormGroup, } from '@angular/forms'; -import { DeviceValidators } from '../../device-repository/device-form/device.validators'; -import { EscapableDialogComponent } from '../../components/escapable-dialog/escapable-dialog.component'; +import { DeviceValidators } from '../../../devices/components/device-form/device.validators'; +import { EscapableDialogComponent } from '../../../../components/escapable-dialog/escapable-dialog.component'; import { take } from 'rxjs'; interface DialogData { diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.html b/modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.html similarity index 100% rename from modules/ui/src/app/progress/progress-status-card/progress-status-card.component.html rename to modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.html diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.scss b/modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.scss similarity index 95% rename from modules/ui/src/app/progress/progress-status-card/progress-status-card.component.scss rename to modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.scss index 74f337d0f..6b2ee3835 100644 --- a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.scss +++ b/modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@use '@angular/material' as mat; -@import '../../../theming/colors'; +@use 'node_modules/@angular/material/index' as mat; +@import 'src/theming/colors'; :host { height: auto; diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.spec.ts b/modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.spec.ts similarity index 98% rename from modules/ui/src/app/progress/progress-status-card/progress-status-card.component.spec.ts rename to modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.spec.ts index 6252136b1..ae8f12461 100644 --- a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.spec.ts @@ -16,15 +16,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProgressStatusCardComponent } from './progress-status-card.component'; -import { StatusOfTestrun, TestrunStatus } from '../../model/testrun-status'; +import { + StatusOfTestrun, + TestrunStatus, +} from '../../../../model/testrun-status'; import { MOCK_PROGRESS_DATA_CANCELLED, MOCK_PROGRESS_DATA_COMPLIANT, MOCK_PROGRESS_DATA_IN_PROGRESS, MOCK_PROGRESS_DATA_MONITORING, MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE, -} from '../../mocks/progress.mock'; -import { ProgressModule } from '../progress.module'; +} from '../../../../mocks/progress.mock'; +import { ProgressModule } from '../../progress.module'; import { of } from 'rxjs'; describe('ProgressStatusCardComponent', () => { diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.ts b/modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.ts similarity index 93% rename from modules/ui/src/app/progress/progress-status-card/progress-status-card.component.ts rename to modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.ts index c2ddd2760..844bb377a 100644 --- a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.ts +++ b/modules/ui/src/app/pages/testrun/components/progress-status-card/progress-status-card.component.ts @@ -20,7 +20,7 @@ import { StatusOfTestrun, TestrunStatus, TestsData, -} from '../../model/testrun-status'; +} from '../../../../model/testrun-status'; @Component({ selector: 'app-progress-status-card', @@ -65,12 +65,13 @@ export class ProgressStatusCardComponent { (data.tests as TestsData)?.results?.length && (data.tests as TestsData)?.total ) { - return `${(data.tests as TestsData)?.results?.length}/${( - data.tests as TestsData - )?.total}`; + return `${(data.tests as TestsData)?.results?.length}/${ + (data.tests as TestsData)?.total + }`; } else if ((data.tests as IResult[])?.length) { - return `${(data.tests as IResult[])?.length}/${(data.tests as IResult[]) - ?.length}`; + return `${(data.tests as IResult[])?.length}/${ + (data.tests as IResult[])?.length + }`; } } return ''; diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.html b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.html similarity index 100% rename from modules/ui/src/app/progress/progress-table/progress-table.component.html rename to modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.html diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.scss b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.scss similarity index 93% rename from modules/ui/src/app/progress/progress-table/progress-table.component.scss rename to modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.scss index 2b608f7e7..ac309d522 100644 --- a/modules/ui/src/app/progress/progress-table/progress-table.component.scss +++ b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '../../../theming/colors'; -@import '../../../theming/variables'; +@import 'src/theming/colors'; +@import 'src/theming/variables'; :host { overflow-y: auto; diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.spec.ts b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.spec.ts similarity index 94% rename from modules/ui/src/app/progress/progress-table/progress-table.component.spec.ts rename to modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.spec.ts index e7f565623..320023cca 100644 --- a/modules/ui/src/app/progress/progress-table/progress-table.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.spec.ts @@ -16,11 +16,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProgressTableComponent } from './progress-table.component'; -import { IResult, StatusOfTestResult } from '../../model/testrun-status'; +import { IResult, StatusOfTestResult } from '../../../../model/testrun-status'; import { MatTableModule } from '@angular/material/table'; import { of } from 'rxjs'; -import { TEST_DATA } from '../../mocks/progress.mock'; -import { TestRunService } from '../../services/test-run.service'; +import { TEST_DATA } from '../../../../mocks/progress.mock'; +import { TestRunService } from '../../../../services/test-run.service'; describe('ProgressTableComponent', () => { let component: ProgressTableComponent; diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.ts b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.ts similarity index 88% rename from modules/ui/src/app/progress/progress-table/progress-table.component.ts rename to modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.ts index 27df71ffc..808e565c0 100644 --- a/modules/ui/src/app/progress/progress-table/progress-table.component.ts +++ b/modules/ui/src/app/pages/testrun/components/progress-table/progress-table.component.ts @@ -15,8 +15,11 @@ */ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; -import { IResult, StatusResultClassName } from '../../model/testrun-status'; -import { TestRunService } from '../../services/test-run.service'; +import { + IResult, + StatusResultClassName, +} from '../../../../model/testrun-status'; +import { TestRunService } from '../../../../services/test-run.service'; @Component({ selector: 'app-progress-table', diff --git a/modules/ui/src/app/progress/progress-routing.module.ts b/modules/ui/src/app/pages/testrun/progress-routing.module.ts similarity index 100% rename from modules/ui/src/app/progress/progress-routing.module.ts rename to modules/ui/src/app/pages/testrun/progress-routing.module.ts diff --git a/modules/ui/src/app/progress/progress.component.html b/modules/ui/src/app/pages/testrun/progress.component.html similarity index 100% rename from modules/ui/src/app/progress/progress.component.html rename to modules/ui/src/app/pages/testrun/progress.component.html diff --git a/modules/ui/src/app/progress/progress.component.scss b/modules/ui/src/app/pages/testrun/progress.component.scss similarity index 96% rename from modules/ui/src/app/progress/progress.component.scss rename to modules/ui/src/app/pages/testrun/progress.component.scss index 50fd91fe1..c5c066b2e 100644 --- a/modules/ui/src/app/progress/progress.component.scss +++ b/modules/ui/src/app/pages/testrun/progress.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@use '@angular/material' as mat; -@import '../../theming/colors'; +@use 'node_modules/@angular/material/index' as mat; +@import 'src/theming/colors'; :host { display: flex; diff --git a/modules/ui/src/app/progress/progress.component.spec.ts b/modules/ui/src/app/pages/testrun/progress.component.spec.ts similarity index 93% rename from modules/ui/src/app/progress/progress.component.spec.ts rename to modules/ui/src/app/pages/testrun/progress.component.spec.ts index 3e453ef6b..fdf77eedb 100644 --- a/modules/ui/src/app/progress/progress.component.spec.ts +++ b/modules/ui/src/app/pages/testrun/progress.component.spec.ts @@ -22,7 +22,7 @@ import { } from '@angular/core/testing'; import { ProgressComponent } from './progress.component'; -import { TestRunService } from '../services/test-run.service'; +import { TestRunService } from '../../services/test-run.service'; import { of } from 'rxjs'; import { EMPTY_RESULT, @@ -35,23 +35,23 @@ import { MOCK_PROGRESS_DATA_NOT_STARTED, MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE, TEST_DATA_TABLE_RESULT, -} from '../mocks/progress.mock'; +} from '../../mocks/progress.mock'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; import { Component, Input } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; -import { IResult, TestrunStatus } from '../model/testrun-status'; +import { IResult, TestrunStatus } from '../../model/testrun-status'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { Device } from '../model/device'; -import { ProgressInitiateFormComponent } from './progress-initiate-form/progress-initiate-form.component'; -import { DownloadReportComponent } from '../components/download-report/download-report.component'; -import { device } from '../mocks/device.mock'; -import { DeleteFormComponent } from '../components/delete-form/delete-form.component'; -import { SpinnerComponent } from '../components/spinner/spinner.component'; -import { LoaderService } from '../services/loader.service'; -import { StateService } from '../services/state.service'; +import { Device } from '../../model/device'; +import { ProgressInitiateFormComponent } from './components/progress-initiate-form/progress-initiate-form.component'; +import { DownloadReportComponent } from '../../components/download-report/download-report.component'; +import { device } from '../../mocks/device.mock'; +import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; +import { SpinnerComponent } from '../../components/spinner/spinner.component'; +import { LoaderService } from '../../services/loader.service'; +import { FocusManagerService } from '../../services/focus-manager.service'; describe('ProgressComponent', () => { let component: ProgressComponent; @@ -73,10 +73,8 @@ describe('ProgressComponent', () => { ['setLoading', 'getLoading'] ); - const stateServiceMock: jasmine.SpyObj = jasmine.createSpyObj( - 'stateServiceMock', - ['focusFirstElementInMain'] - ); + const stateServiceMock: jasmine.SpyObj = + jasmine.createSpyObj('stateServiceMock', ['focusFirstElementInContainer']); testRunServiceMock.getDevices.and.returnValue( new BehaviorSubject([]) @@ -101,7 +99,7 @@ describe('ProgressComponent', () => { ], providers: [ { provide: TestRunService, useValue: testRunServiceMock }, - { provide: StateService, useValue: stateServiceMock }, + { provide: FocusManagerService, useValue: stateServiceMock }, { provide: MatDialogRef, useValue: {}, @@ -242,14 +240,16 @@ describe('ProgressComponent', () => { }); }); - it('should call focusFirstElementInMain when testrun stops after cancelling', () => { + it('should call focusFirstElementInContainer when testrun stops after cancelling', () => { testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_COMPLIANT); component.isCancelling = true; component.ngOnInit(); fixture.detectChanges(); - expect(stateServiceMock.focusFirstElementInMain).toHaveBeenCalled(); + expect( + stateServiceMock.focusFirstElementInContainer + ).toHaveBeenCalled(); }); describe('hideLoading', () => { @@ -312,7 +312,7 @@ describe('ProgressComponent', () => { ], providers: [ { provide: TestRunService, useValue: testRunServiceMock }, - { provide: StateService, useValue: stateServiceMock }, + { provide: FocusManagerService, useValue: stateServiceMock }, { provide: MatDialogRef, useValue: {}, @@ -409,7 +409,9 @@ describe('ProgressComponent', () => { panelClass: 'initiate-test-run-dialog', }); tick(10); - expect(stateServiceMock.focusFirstElementInMain).toHaveBeenCalled(); + expect( + stateServiceMock.focusFirstElementInContainer + ).toHaveBeenCalled(); openSpy.calls.reset(); })); diff --git a/modules/ui/src/app/progress/progress.component.ts b/modules/ui/src/app/pages/testrun/progress.component.ts similarity index 90% rename from modules/ui/src/app/progress/progress.component.ts rename to modules/ui/src/app/pages/testrun/progress.component.ts index 79141c1f1..7c324e556 100644 --- a/modules/ui/src/app/progress/progress.component.ts +++ b/modules/ui/src/app/pages/testrun/progress.component.ts @@ -20,14 +20,14 @@ import { OnInit, } from '@angular/core'; import { Observable } from 'rxjs/internal/Observable'; -import { TestRunService } from '../services/test-run.service'; +import { TestRunService } from '../../services/test-run.service'; import { IResult, StatusOfTestrun, TestrunStatus, TestsData, TestsResponse, -} from '../model/testrun-status'; +} from '../../model/testrun-status'; import { interval, map, @@ -38,12 +38,12 @@ import { timer, } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; -import { ProgressInitiateFormComponent } from './progress-initiate-form/progress-initiate-form.component'; -import { DeleteFormComponent } from '../components/delete-form/delete-form.component'; -import { LoaderService } from '../services/loader.service'; -import { LOADER_TIMEOUT_CONFIG_TOKEN } from '../services/loaderConfig'; -import { Device } from '../model/device'; -import { StateService } from '../services/state.service'; +import { ProgressInitiateFormComponent } from './components/progress-initiate-form/progress-initiate-form.component'; +import { DeleteFormComponent } from '../../components/delete-form/delete-form.component'; +import { LoaderService } from '../../services/loader.service'; +import { LOADER_TIMEOUT_CONFIG_TOKEN } from '../../services/loaderConfig'; +import { Device } from '../../model/device'; +import { FocusManagerService } from '../../services/focus-manager.service'; import { combineLatest } from 'rxjs/internal/observable/combineLatest'; const EMPTY_RESULT = new Array(100).fill(null).map(() => ({}) as IResult); @@ -74,7 +74,7 @@ export class ProgressComponent implements OnInit, OnDestroy { private readonly testRunService: TestRunService, private readonly loaderService: LoaderService, public dialog: MatDialog, - private readonly state: StateService + private readonly state: FocusManagerService ) {} ngOnInit(): void { @@ -116,7 +116,7 @@ export class ProgressComponent implements OnInit, OnDestroy { res.status !== StatusOfTestrun.Cancelling ) { if (this.isCancelling) { - this.state.focusFirstElementInMain(); + this.state.focusFirstElementInContainer(); } this.isCancelling = false; this.destroyInterval$.next(true); @@ -256,7 +256,7 @@ export class ProgressComponent implements OnInit, OnDestroy { timer(10) .pipe(takeUntil(this.destroy$)) .subscribe(() => { - this.state.focusFirstElementInMain(); + this.state.focusFirstElementInContainer(); }); }); } diff --git a/modules/ui/src/app/progress/progress.module.ts b/modules/ui/src/app/pages/testrun/progress.module.ts similarity index 72% rename from modules/ui/src/app/progress/progress.module.ts rename to modules/ui/src/app/pages/testrun/progress.module.ts index e7b28e662..9e1106589 100644 --- a/modules/ui/src/app/progress/progress.module.ts +++ b/modules/ui/src/app/pages/testrun/progress.module.ts @@ -23,16 +23,16 @@ import { MatTableModule } from '@angular/material/table'; import { ProgressRoutingModule } from './progress-routing.module'; import { ProgressComponent } from './progress.component'; -import { ProgressStatusCardComponent } from './progress-status-card/progress-status-card.component'; -import { ProgressTableComponent } from './progress-table/progress-table.component'; -import { ProgressInitiateFormComponent } from './progress-initiate-form/progress-initiate-form.component'; +import { ProgressStatusCardComponent } from './components/progress-status-card/progress-status-card.component'; +import { ProgressTableComponent } from './components/progress-table/progress-table.component'; +import { ProgressInitiateFormComponent } from './components/progress-initiate-form/progress-initiate-form.component'; import { MatDialogModule } from '@angular/material/dialog'; -import { DeviceItemComponent } from '../components/device-item/device-item.component'; +import { DeviceItemComponent } from '../../components/device-item/device-item.component'; import { MatInputModule } from '@angular/material/input'; import { ReactiveFormsModule } from '@angular/forms'; -import { DeviceTestsComponent } from '../components/device-tests/device-tests.component'; -import { DownloadReportComponent } from '../components/download-report/download-report.component'; -import { SpinnerComponent } from '../components/spinner/spinner.component'; +import { DeviceTestsComponent } from '../../components/device-tests/device-tests.component'; +import { DownloadReportComponent } from '../../components/download-report/download-report.component'; +import { SpinnerComponent } from '../../components/spinner/spinner.component'; @NgModule({ declarations: [ diff --git a/modules/ui/src/app/services/focus-manager.service.spec.ts b/modules/ui/src/app/services/focus-manager.service.spec.ts new file mode 100644 index 000000000..95c83919a --- /dev/null +++ b/modules/ui/src/app/services/focus-manager.service.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FocusManagerService } from './focus-manager.service'; +import { Component } from '@angular/core'; + +@Component({ + template: + '
' + + '
', +}) +class DummyComponent {} + +describe('FocusManagerService', () => { + let service: FocusManagerService; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DummyComponent], + }); + service = TestBed.inject(FocusManagerService); + + fixture = TestBed.createComponent(DummyComponent); + + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should focus element in document if container is not provided', () => { + const testButton = fixture.nativeElement.querySelector( + '#main .test-button' + ) as HTMLButtonElement; + + service.focusFirstElementInContainer(); + + expect(document.activeElement).toBe(testButton); + }); + + it('should focus element in container if container is provided', () => { + const container = fixture.nativeElement.querySelector( + '#second' + ) as HTMLElement; + + const testButton = fixture.nativeElement.querySelector( + '#second .test-button' + ) as HTMLButtonElement; + + service.focusFirstElementInContainer(container); + + expect(document.activeElement).toBe(testButton); + }); +}); diff --git a/modules/ui/src/app/services/focus-manager.service.ts b/modules/ui/src/app/services/focus-manager.service.ts new file mode 100644 index 000000000..b602bdacf --- /dev/null +++ b/modules/ui/src/app/services/focus-manager.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class FocusManagerService { + focusFirstElementInContainer( + container: Document | Element | null = window.document.querySelector( + '#main' + ) + ) { + const firstControl: HTMLElement | undefined | null = + container?.querySelector( + 'button:not([disabled="true"]), a:not([disabled="true"]), table' + ); + + if (firstControl) { + firstControl.focus(); + } + } +} diff --git a/modules/ui/src/app/services/notification.service.spec.ts b/modules/ui/src/app/services/notification.service.spec.ts index 53cc73970..c2e35a4cf 100644 --- a/modules/ui/src/app/services/notification.service.spec.ts +++ b/modules/ui/src/app/services/notification.service.spec.ts @@ -50,9 +50,9 @@ describe('NotificationService', () => { expect(args[0]).toBe('something good happened'); expect(args[1]).toBe('OK'); expect(args[2]).toEqual({ - horizontalPosition: 'right', + horizontalPosition: 'center', panelClass: 'test-run-notification', - duration: 5000, + duration: 0, }); }); @@ -63,7 +63,7 @@ describe('NotificationService', () => { const args = matSnackBarSpy.calls.argsFor(0); expect(args[2]).toEqual({ - horizontalPosition: 'right', + horizontalPosition: 'center', panelClass: 'test-run-notification', duration: 15000, }); diff --git a/modules/ui/src/app/services/notification.service.ts b/modules/ui/src/app/services/notification.service.ts index 7daa17444..4715087c2 100644 --- a/modules/ui/src/app/services/notification.service.ts +++ b/modules/ui/src/app/services/notification.service.ts @@ -22,9 +22,9 @@ import { MatSnackBar } from '@angular/material/snack-bar'; export class NotificationService { constructor(private snackBar: MatSnackBar) {} - notify(message: string, duration = 5000) { + notify(message: string, duration = 0) { this.snackBar.open(message, 'OK', { - horizontalPosition: 'right', + horizontalPosition: 'center', panelClass: 'test-run-notification', duration: duration, }); diff --git a/modules/ui/src/app/services/state.service.spec.ts b/modules/ui/src/app/services/state.service.spec.ts deleted file mode 100644 index 0a5c5bc0a..000000000 --- a/modules/ui/src/app/services/state.service.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { StateService } from './state.service'; -import { Component } from '@angular/core'; - -@Component({ - template: '
', -}) -class DummyComponent {} - -describe('StateService', () => { - let service: StateService; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [DummyComponent], - }); - service = TestBed.inject(StateService); - - fixture = TestBed.createComponent(DummyComponent); - - fixture.detectChanges(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should focus element', () => { - const testButton = fixture.nativeElement.querySelector( - '#test-button' - ) as HTMLButtonElement; - - service.focusFirstElementInMain(); - - expect(document.activeElement).toBe(testButton); - }); -}); diff --git a/modules/ui/src/app/services/state.service.ts b/modules/ui/src/app/services/state.service.ts deleted file mode 100644 index 509fbd897..000000000 --- a/modules/ui/src/app/services/state.service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root', -}) -export class StateService { - focusFirstElementInMain() { - const firstControl: HTMLElement | null = window.document.querySelector( - '#main button:not([disabled="true"]), ' + - '#main a:not([disabled="true"]), #main table' - ); - - if (firstControl) { - firstControl.focus(); - } - } -} 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 5cb091fed..19b08b3a8 100644 --- a/modules/ui/src/app/services/test-run.service.spec.ts +++ b/modules/ui/src/app/services/test-run.service.spec.ts @@ -117,22 +117,6 @@ describe('TestRunService', () => { req.flush(deviceArray); }); - it('setSystemConfig should update the systemConfig data', () => { - service.setSystemConfig(MOCK_SYSTEM_CONFIG); - - service.systemConfig$.subscribe(data => { - expect(data).toEqual(MOCK_SYSTEM_CONFIG); - }); - }); - - it('setHasConnectionSetting should update the hasConnectionSetting$', () => { - service.setHasConnectionSetting(true); - - service.hasConnectionSetting$.subscribe(data => { - expect(data).toEqual(true); - }); - }); - it('getSystemConfig should return systemConfig data', () => { const apiUrl = 'http://localhost:8000/system/config'; diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index 37d46f23b..6d6316084 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -71,14 +71,10 @@ export class TestRunService { public isOpenAddDevice$ = this.isOpenAddDeviceSub$.asObservable(); private isOpenStartTestrunSub$ = new BehaviorSubject(false); public isOpenStartTestrun$ = this.isOpenStartTestrunSub$.asObservable(); - private _systemConfig = new BehaviorSubject(null); - public systemConfig$ = this._systemConfig.asObservable(); private systemStatusSubject = new ReplaySubject(1); public systemStatus$ = this.systemStatusSubject.asObservable(); private isTestrunStartedSub$ = new BehaviorSubject(false); public isTestrunStarted$ = this.isTestrunStartedSub$.asObservable(); - private hasConnectionSettingSub$ = new BehaviorSubject(null); - public hasConnectionSetting$ = this.hasConnectionSettingSub$.asObservable(); private history = new BehaviorSubject(null); private version = new BehaviorSubject(null); @@ -92,9 +88,6 @@ export class TestRunService { this.isOpenStartTestrunSub$.next(isOpen); } - setHasConnectionSetting(hasSetting: boolean): void { - this.hasConnectionSettingSub$.next(hasSetting); - } getDevices(): BehaviorSubject { return this.devices; } @@ -103,10 +96,6 @@ export class TestRunService { this.devices.next(devices); } - setSystemConfig(config: SystemConfig): void { - this._systemConfig.next(config); - } - setSystemStatus(status: TestrunStatus): void { this.systemStatusSubject.next(status); } diff --git a/modules/ui/src/app/store/actions.ts b/modules/ui/src/app/store/actions.ts new file mode 100644 index 000000000..8566b08df --- /dev/null +++ b/modules/ui/src/app/store/actions.ts @@ -0,0 +1,72 @@ +/* + * 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 { createAction, props } from '@ngrx/store'; +import { SystemConfig } from '../model/setting'; +import { SystemInterfaces } from '../services/test-run.service'; + +// App component +export const toggleMenu = createAction('[App Component] Toggle Menu'); + +export const fetchInterfaces = createAction('[App Component] Fetch Interfaces'); + +export const fetchInterfacesSuccess = createAction( + '[App Component] Fetch interfaces Success', + props<{ interfaces: SystemInterfaces }>() +); + +export const updateFocusNavigation = createAction( + '[App Component] update focus navigation', + props<{ focusNavigation: boolean }>() +); + +export const updateValidInterfaces = createAction( + '[App Component] Update Valid Interfaces', + props<{ validInterfaces: boolean }>() +); + +export const updateError = createAction( + '[App Component] Update Error', + props<{ error: boolean }>() +); + +// Settings +export const fetchSystemConfig = createAction('[Settings] Fetch System Config'); + +export const fetchSystemConfigSuccess = createAction( + '[Settings] Fetch System Config Success', + props<{ systemConfig: SystemConfig }>() +); + +export const createSystemConfig = createAction( + '[Settings] Create System Config', + props<{ + data: SystemConfig; + }>() +); + +export const createSystemConfigSuccess = createAction( + '[Settings] Create System Config Success', + props<{ + data: SystemConfig; + }>() +); + +// Shared +export const setHasConnectionSettings = createAction( + '[Shared] Set Has Connection Settings', + props<{ hasConnectionSettings: boolean }>() +); diff --git a/modules/ui/src/app/store/effects.spec.ts b/modules/ui/src/app/store/effects.spec.ts new file mode 100644 index 000000000..eb4b0932b --- /dev/null +++ b/modules/ui/src/app/store/effects.spec.ts @@ -0,0 +1,210 @@ +/* + * 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 { provideMockActions } from '@ngrx/effects/testing'; +import { Observable, of } from 'rxjs'; +import { AppEffects } from './effects'; +import { TestRunService } from '../services/test-run.service'; +import SpyObj = jasmine.SpyObj; +import { Action } from '@ngrx/store'; +import * as actions from './actions'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { AppState } from './state'; +import { selectMenuOpened, selectSystemConfig } from './selectors'; +describe('Effects', () => { + let actions$ = new Observable(); + let effects: AppEffects; + let testRunServiceMock: SpyObj; + let store: MockStore; + + beforeEach(() => { + testRunServiceMock = jasmine.createSpyObj([ + 'getSystemInterfaces', + 'getSystemConfig', + 'createSystemConfig', + ]); + testRunServiceMock.getSystemInterfaces.and.returnValue(of({})); + testRunServiceMock.getSystemConfig.and.returnValue(of({})); + testRunServiceMock.createSystemConfig.and.returnValue(of({})); + TestBed.configureTestingModule({ + providers: [ + AppEffects, + { provide: TestRunService, useValue: testRunServiceMock }, + provideMockActions(() => actions$), + provideMockStore({}), + ], + }); + + store = TestBed.inject(MockStore); + effects = TestBed.inject(AppEffects); + + store.refreshState(); + }); + + it('onFetchInterfaces$ should call fetchInterfacesSuccess on success', done => { + actions$ = of(actions.fetchInterfaces()); + + effects.onFetchInterfaces$.subscribe(action => { + expect(action).toEqual( + actions.fetchInterfacesSuccess({ interfaces: {} }) + ); + done(); + }); + }); + + it('onMenuOpened$ should call updateFocusNavigation', done => { + actions$ = of(actions.toggleMenu()); + store.overrideSelector(selectMenuOpened, true); + + effects.onMenuOpened$.subscribe(action => { + expect(action).toEqual( + actions.updateFocusNavigation({ focusNavigation: true }) + ); + done(); + }); + }); + + it('onFetchSystemConfig$ should call fetchSystemConfigSuccess', done => { + actions$ = of(actions.fetchSystemConfig); + + effects.onFetchSystemConfig$.subscribe(action => { + expect(action).toEqual( + actions.fetchSystemConfigSuccess({ systemConfig: {} }) + ); + done(); + }); + }); + + it('onFetchSystemConfigSuccess$ should call setHasConnectionSettings with true if config is not empty', done => { + actions$ = of(actions.fetchSystemConfigSuccess); + store.overrideSelector(selectSystemConfig, { + network: { device_intf: '123', internet_intf: '123' }, + }); + + effects.onFetchSystemConfigSuccessNonEmpty$.subscribe(action => { + expect(action).toEqual( + actions.setHasConnectionSettings({ hasConnectionSettings: true }) + ); + done(); + }); + }); + + it('onFetchSystemConfigSuccess$ should call setHasConnectionSettings with false if config is empty', done => { + actions$ = of(actions.fetchSystemConfigSuccess); + store.overrideSelector(selectSystemConfig, {}); + + effects.onFetchSystemConfigSuccessEmpty$.subscribe(action => { + expect(action).toEqual( + actions.setHasConnectionSettings({ hasConnectionSettings: false }) + ); + done(); + }); + }); + + it('onCreateSystemConfig$ should call createSystemConfigSuccess', done => { + actions$ = of(actions.createSystemConfig({ data: {} })); + + effects.onCreateSystemConfig$.subscribe(action => { + expect(action).toEqual(actions.createSystemConfigSuccess({ data: {} })); + done(); + }); + }); + + it('onCreateSystemConfigSuccess$ should call fetchSystemConfigSuccess', done => { + actions$ = of(actions.createSystemConfigSuccess({ data: {} })); + + effects.onCreateSystemConfigSuccess$.subscribe(action => { + expect(action).toEqual( + actions.fetchSystemConfigSuccess({ systemConfig: {} }) + ); + done(); + }); + }); + + describe('onValidateInterfaces$', () => { + it('should call updateError with false if interfaces are valid', done => { + actions$ = of(actions.updateValidInterfaces({ validInterfaces: true })); + + effects.onValidateInterfaces$.subscribe(action => { + expect(action).toEqual(actions.updateError({ error: false })); + done(); + }); + }); + + it('should call updateError with true if interfaces are not valid', done => { + actions$ = of(actions.updateValidInterfaces({ validInterfaces: false })); + + effects.onValidateInterfaces$.subscribe(action => { + expect(action).toEqual(actions.updateError({ error: true })); + done(); + }); + }); + }); + + describe('checkInterfacesInConfig$', () => { + it('should call updateValidInterfaces with false if interface is no longer available', done => { + actions$ = of( + actions.fetchInterfacesSuccess({ + interfaces: { + enx00e04c020fa8: '00:e0:4c:02:0f:a8', + enx207bd26205e9: '20:7b:d2:62:05:e9', + }, + }), + actions.fetchSystemConfigSuccess({ + systemConfig: { + network: { + device_intf: 'enx00e04c020fa2', + internet_intf: 'enx207bd26205e9', + }, + }, + }) + ); + + effects.checkInterfacesInConfig$.subscribe(action => { + expect(action).toEqual( + actions.updateValidInterfaces({ validInterfaces: false }) + ); + done(); + }); + }); + + it('should call updateValidInterfaces with true if interface is available', done => { + actions$ = of( + actions.fetchInterfacesSuccess({ + interfaces: { + enx00e04c020fa8: '00:e0:4c:02:0f:a8', + enx207bd26205e9: '20:7b:d2:62:05:e9', + }, + }), + actions.fetchSystemConfigSuccess({ + systemConfig: { + network: { + device_intf: 'enx00e04c020fa8', + internet_intf: 'enx207bd26205e9', + }, + }, + }) + ); + + effects.checkInterfacesInConfig$.subscribe(action => { + expect(action).toEqual( + actions.updateValidInterfaces({ validInterfaces: true }) + ); + done(); + }); + }); + }); +}); diff --git a/modules/ui/src/app/store/effects.ts b/modules/ui/src/app/store/effects.ts new file mode 100644 index 000000000..8d6d80798 --- /dev/null +++ b/modules/ui/src/app/store/effects.ts @@ -0,0 +1,157 @@ +/* + * 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 { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { map, switchMap, withLatestFrom } from 'rxjs/operators'; + +import * as AppActions from './actions'; +import { AppState } from './state'; +import { TestRunService } from '../services/test-run.service'; +import { filter, combineLatest } from 'rxjs'; +import { selectMenuOpened, selectSystemConfig } from './selectors'; + +@Injectable() +export class AppEffects { + checkInterfacesInConfig$ = createEffect(() => + combineLatest([ + this.actions$.pipe(ofType(AppActions.fetchInterfacesSuccess)), + this.actions$.pipe(ofType(AppActions.fetchSystemConfigSuccess)), + ]).pipe( + map( + ([ + { interfaces }, + { + systemConfig: { network }, + }, + ]) => + AppActions.updateValidInterfaces({ + validInterfaces: + network != null && + // @ts-expect-error network is not null + interfaces[network.device_intf] != null && + (network.internet_intf == '' || + // @ts-expect-error network is not null + interfaces[network.internet_intf] != null), + }) + ) + ) + ); + + onValidateInterfaces$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.updateValidInterfaces), + map(({ validInterfaces }) => + AppActions.updateError({ error: !validInterfaces }) + ) + ); + }); + + onFetchInterfaces$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.fetchInterfaces), + switchMap(() => + this.testrunService + .getSystemInterfaces() + .pipe( + map(interfaces => AppActions.fetchInterfacesSuccess({ interfaces })) + ) + ) + ); + }); + onMenuOpened$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.toggleMenu), + withLatestFrom(this.store.select(selectMenuOpened)), + filter(([, opened]) => opened === true), + map(() => AppActions.updateFocusNavigation({ focusNavigation: true })) // user will be navigated to side menu on tab + ); + }); + + onFetchSystemConfig$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.fetchSystemConfig), + switchMap(() => + this.testrunService + .getSystemConfig() + .pipe( + map(systemConfig => + AppActions.fetchSystemConfigSuccess({ systemConfig }) + ) + ) + ) + ); + }); + + onFetchSystemConfigSuccessNonEmpty$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.fetchSystemConfigSuccess), + withLatestFrom(this.store.select(selectSystemConfig)), + filter( + ([, systemConfig]) => + systemConfig.network != null && systemConfig.network.device_intf != '' + ), + map(() => + AppActions.setHasConnectionSettings({ hasConnectionSettings: true }) + ) + ); + }); + + onFetchSystemConfigSuccessEmpty$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.fetchSystemConfigSuccess), + withLatestFrom(this.store.select(selectSystemConfig)), + filter( + ([, systemConfig]) => + systemConfig.network == null || + systemConfig.network.device_intf === '' + ), + map(() => + AppActions.setHasConnectionSettings({ hasConnectionSettings: false }) + ) + ); + }); + + onCreateSystemConfig$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.createSystemConfig), + switchMap(action => + this.testrunService + .createSystemConfig(action.data) + .pipe( + map(() => + AppActions.createSystemConfigSuccess({ data: action.data }) + ) + ) + ) + ); + }); + + onCreateSystemConfigSuccess$ = createEffect(() => { + return this.actions$.pipe( + ofType(AppActions.createSystemConfigSuccess), + map(action => + AppActions.fetchSystemConfigSuccess({ systemConfig: action.data }) + ) + ); + }); + constructor( + private actions$: Actions, + private testrunService: TestRunService, + private store: Store + ) {} +} diff --git a/modules/ui/src/app/store/reducers.spec.ts b/modules/ui/src/app/store/reducers.spec.ts new file mode 100644 index 000000000..42dadcd41 --- /dev/null +++ b/modules/ui/src/app/store/reducers.spec.ts @@ -0,0 +1,124 @@ +/* + * 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 * as fromReducer from './reducers'; +import { + initialAppComponentState, + initialSettingsState, + initialSharedState, +} from './state'; +import { + fetchInterfacesSuccess, + fetchSystemConfigSuccess, + setHasConnectionSettings, + toggleMenu, + updateError, + updateFocusNavigation, +} from './actions'; + +describe('Reducer', () => { + describe('unknown action', () => { + it('should return the default state', () => { + const initialState = initialAppComponentState; + const action = { + type: 'Unknown', + }; + const state = fromReducer.appComponentReducer(initialState, action); + + expect(state).toBe(initialState); + }); + }); + + describe('fetchInterfacesSuccess action', () => { + it('should update state', () => { + const initialState = initialAppComponentState; + const newInterfaces = { + enx00e04c020fa8: '00:e0:4c:02:0f:a8', + enx207bd26205e9: '20:7b:d2:62:05:e9', + }; + const action = fetchInterfacesSuccess({ interfaces: newInterfaces }); + const state = fromReducer.appComponentReducer(initialState, action); + + const newState = { ...initialState, ...{ interfaces: newInterfaces } }; + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('updateFocusNavigation action', () => { + it('should update state', () => { + const initialState = initialAppComponentState; + const action = updateFocusNavigation({ focusNavigation: true }); + const state = fromReducer.appComponentReducer(initialState, action); + + const newState = { ...initialState, ...{ focusNavigation: true } }; + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('toggleMenu action', () => { + it('should update state', () => { + const initialState = initialAppComponentState; + const action = toggleMenu(); + const state = fromReducer.appComponentReducer(initialState, action); + + const newState = { ...initialState, ...{ isMenuOpen: true } }; + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('setHasConnectionSettings action', () => { + it('should update state', () => { + const initialState = initialSharedState; + const action = setHasConnectionSettings({ hasConnectionSettings: true }); + const state = fromReducer.sharedReducer(initialState, action); + const newState = { ...initialState, ...{ hasConnectionSettings: true } }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('fetchSystemConfigSuccess action', () => { + it('should update state', () => { + const initialState = initialSettingsState; + const action = fetchSystemConfigSuccess({ + systemConfig: { network: { device_intf: '111' } }, + }); + const state = fromReducer.settingsReducer(initialState, action); + + const newState = { + ...initialState, + ...{ systemConfig: { network: { device_intf: '111' } } }, + }; + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); + + describe('updateError action', () => { + it('should update state', () => { + const initialState = initialAppComponentState; + const action = updateError({ error: true }); + const state = fromReducer.appComponentReducer(initialState, action); + const newState = { ...initialState, ...{ error: true } }; + + expect(state).toEqual(newState); + expect(state).not.toBe(initialState); + }); + }); +}); diff --git a/modules/ui/src/app/store/reducers.ts b/modules/ui/src/app/store/reducers.ts new file mode 100644 index 000000000..5068cdb77 --- /dev/null +++ b/modules/ui/src/app/store/reducers.ts @@ -0,0 +1,66 @@ +/* + * 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 { combineReducers, createReducer, on } from '@ngrx/store'; +import * as Actions from './actions'; +import { + initialAppComponentState, + initialSettingsState, + initialSharedState, +} from './state'; + +export const appFeatureKey = 'app'; + +export const appComponentReducer = createReducer( + initialAppComponentState, + on(Actions.toggleMenu, state => ({ + ...state, + isMenuOpen: !state.isMenuOpen, + })), + on(Actions.fetchInterfacesSuccess, (state, { interfaces }) => ({ + ...state, + interfaces, + })), + on(Actions.updateFocusNavigation, (state, { focusNavigation }) => ({ + ...state, + focusNavigation, + })), + on(Actions.updateError, (state, { error }) => ({ + ...state, + error, + })) +); + +export const sharedReducer = createReducer( + initialSharedState, + on(Actions.setHasConnectionSettings, (state, { hasConnectionSettings }) => ({ + ...state, + hasConnectionSettings, + })) +); + +export const settingsReducer = createReducer( + initialSettingsState, + on(Actions.fetchSystemConfigSuccess, (state, { systemConfig }) => ({ + ...state, + systemConfig, + })) +); + +export const rootReducer = combineReducers({ + appComponent: appComponentReducer, + shared: sharedReducer, + settings: settingsReducer, +}); diff --git a/modules/ui/src/app/store/selectors.spec.ts b/modules/ui/src/app/store/selectors.spec.ts new file mode 100644 index 000000000..23b567d3f --- /dev/null +++ b/modules/ui/src/app/store/selectors.spec.ts @@ -0,0 +1,68 @@ +/* + * 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 { AppState } from './state'; +import { + selectError, + selectHasConnectionSettings, + selectInterfaces, + selectMenuOpened, + selectSystemConfig, +} from './selectors'; + +describe('Selectors', () => { + const initialState: AppState = { + appComponent: { + isMenuOpen: false, + interfaces: {}, + isStatusLoaded: false, + devicesLength: 0, + focusNavigation: false, + error: false, + }, + settings: { + systemConfig: {}, + }, + shared: { + hasConnectionSettings: false, + }, + }; + + it('should select the is menu opened', () => { + const result = selectMenuOpened.projector(initialState); + expect(result).toEqual(false); + }); + + it('should select interfaces', () => { + const result = selectInterfaces.projector(initialState); + expect(result).toEqual({}); + }); + + it('should select system config', () => { + const result = selectSystemConfig.projector(initialState); + expect(result).toEqual({}); + }); + + it('should select has connection settings', () => { + const result = selectHasConnectionSettings.projector(initialState); + expect(result).toEqual(false); + }); + + it('should select error', () => { + const result = selectError.projector(initialState); + expect(result).toEqual(false); + }); +}); diff --git a/modules/ui/src/app/store/selectors.ts b/modules/ui/src/app/store/selectors.ts new file mode 100644 index 000000000..d93cc6eca --- /dev/null +++ b/modules/ui/src/app/store/selectors.ts @@ -0,0 +1,48 @@ +/* + * 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 { createFeatureSelector, createSelector } from '@ngrx/store'; +import * as fromApp from './reducers'; +import { AppState } from './state'; + +export const selectAppState = createFeatureSelector( + fromApp.appFeatureKey +); + +export const selectMenuOpened = createSelector( + selectAppState, + (state: AppState) => state.appComponent.isMenuOpen +); + +export const selectInterfaces = createSelector( + selectAppState, + (state: AppState) => state.appComponent.interfaces +); + +export const selectSystemConfig = createSelector( + selectAppState, + (state: AppState) => state.settings.systemConfig +); + +export const selectHasConnectionSettings = createSelector( + selectAppState, + (state: AppState) => state.shared.hasConnectionSettings +); + +export const selectError = createSelector( + selectAppState, + (state: AppState) => state.appComponent.error +); diff --git a/modules/ui/src/app/store/state.ts b/modules/ui/src/app/store/state.ts new file mode 100644 index 000000000..196346bf6 --- /dev/null +++ b/modules/ui/src/app/store/state.ts @@ -0,0 +1,70 @@ +/* + * 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 { Device } from '../model/device'; +import { TestrunStatus } from '../model/testrun-status'; +import { SystemInterfaces } from '../services/test-run.service'; +import { SystemConfig } from '../model/setting'; + +export interface AppState { + appComponent: AppComponentState; + settings: SettingsState; + shared: SharedState; +} + +export interface AppComponentState { + isMenuOpen: boolean; + interfaces: SystemInterfaces; + /** + * Indicates, if side menu should be focused on keyboard navigation after menu is opened + */ + focusNavigation: boolean; + error: boolean; + isStatusLoaded: boolean; // TODO should be updated in effect when fetch status + devicesLength: number; // TODO should be renamed to focusToggleSettingsBtn (true when devices.length > 0) and updated in effect when fetch device +} + +export interface SharedState { + //used in app, devices, testrun + devices?: Device[] | null; + //app, testrun + systemStatus?: TestrunStatus | null; + //app, testrun + isTestrunStarted?: boolean; + //app, settings + hasConnectionSettings: boolean; +} + +export interface SettingsState { + systemConfig: SystemConfig; +} + +export const initialAppComponentState: AppComponentState = { + isMenuOpen: false, + interfaces: {}, + focusNavigation: false, + isStatusLoaded: false, + devicesLength: 0, + error: false, +}; + +export const initialSettingsState: SettingsState = { + systemConfig: {}, +}; + +export const initialSharedState: SharedState = { + hasConnectionSettings: false, +}; diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss index 7d4a4eb6d..e5d02b971 100644 --- a/modules/ui/src/styles.scss +++ b/modules/ui/src/styles.scss @@ -193,13 +193,30 @@ body:has(.filter-dialog-content) opacity: 0; } -body:has(app-callout .info) +body:has(app-callout .info):not(:has(app-callout .error)) app-device-repository:not(:has(.device-repository-content-empty)), -body:has(app-callout .info) app-history:not(:has(.results-content-empty)), -body:has(app-callout .info) app-progress:not(:has(.progress-content-empty)) { +body:has(app-callout .info):not(:has(app-callout .error)) + app-history:not(:has(.results-content-empty)), +body:has(app-callout .info):not(:has(app-callout .error)) + app-progress:not(:has(.progress-content-empty)), +body:has(app-callout .error):not(:has(app-callout .info)) + app-device-repository:not(:has(.device-repository-content-empty)), +body:has(app-callout .error):not(:has(app-callout .info)) + app-history:not(:has(.results-content-empty)), +body:has(app-callout .error):not(:has(app-callout .info)) + app-progress:not(:has(.progress-content-empty)) { margin-top: 96px; } +body:has(app-callout .info):has(app-callout .error) + app-device-repository:not(:has(.device-repository-content-empty)), +body:has(app-callout .info):has(app-callout .error) + app-history:not(:has(.results-content-empty)), +body:has(app-callout .info):has(app-callout .error) + app-progress:not(:has(.progress-content-empty)) { + margin-top: 156px; +} + .text-nowrap { white-space: nowrap; } diff --git a/modules/ui/tsconfig.json b/modules/ui/tsconfig.json index 1301bf238..8901ce8c6 100644 --- a/modules/ui/tsconfig.json +++ b/modules/ui/tsconfig.json @@ -19,12 +19,12 @@ "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, - "lib": ["ES2022", "dom"] + "lib": ["ES2022", "dom"], }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, - "strictTemplates": true - } + "strictTemplates": true, + }, }