+ + Selected port is missing! Please define a valid one using + Settings + panel. + diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts index 9dee50b99..787155bdd 100644 --- a/modules/ui/src/app/app.component.spec.ts +++ b/modules/ui/src/app/app.component.spec.ts @@ -551,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 () => { diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts index cda9008ae..29d4a21dc 100644 --- a/modules/ui/src/app/app.component.ts +++ b/modules/ui/src/app/app.component.ts @@ -30,6 +30,7 @@ import { FocusManagerService } from './services/focus-manager.service'; import { State, Store } from '@ngrx/store'; import { AppState } from './store/state'; import { + selectError, selectHasConnectionSettings, selectInterfaces, selectMenuOpened, @@ -71,6 +72,7 @@ export class AppComponent implements OnInit { 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; @@ -140,6 +142,7 @@ export class AppComponent implements OnInit { this.getSystemInterfaces(); this.store.dispatch(fetchSystemConfig()); + //this.store.dispatch(fetchSystemConfigAndInterfaces()); } navigateToDeviceRepository(): void { 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/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/store/actions.ts b/modules/ui/src/app/store/actions.ts index 4ee9f1505..8566b08df 100644 --- a/modules/ui/src/app/store/actions.ts +++ b/modules/ui/src/app/store/actions.ts @@ -33,6 +33,16 @@ export const updateFocusNavigation = createAction( 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'); diff --git a/modules/ui/src/app/store/effects.spec.ts b/modules/ui/src/app/store/effects.spec.ts index 82c9e441d..eb4b0932b 100644 --- a/modules/ui/src/app/store/effects.spec.ts +++ b/modules/ui/src/app/store/effects.spec.ts @@ -115,7 +115,7 @@ describe('Effects', () => { }); it('onCreateSystemConfig$ should call createSystemConfigSuccess', done => { - actions$ = of(actions.createSystemConfig); + actions$ = of(actions.createSystemConfig({ data: {} })); effects.onCreateSystemConfig$.subscribe(action => { expect(action).toEqual(actions.createSystemConfigSuccess({ data: {} })); @@ -133,4 +133,78 @@ describe('Effects', () => { 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 index e04f5d080..46ece1368 100644 --- a/modules/ui/src/app/store/effects.ts +++ b/modules/ui/src/app/store/effects.ts @@ -22,11 +22,45 @@ 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 } from 'rxjs'; +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), diff --git a/modules/ui/src/app/store/reducers.spec.ts b/modules/ui/src/app/store/reducers.spec.ts index 1645261a2..42dadcd41 100644 --- a/modules/ui/src/app/store/reducers.spec.ts +++ b/modules/ui/src/app/store/reducers.spec.ts @@ -24,6 +24,7 @@ import { fetchSystemConfigSuccess, setHasConnectionSettings, toggleMenu, + updateError, updateFocusNavigation, } from './actions'; @@ -108,4 +109,16 @@ describe('Reducer', () => { 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 index e0b043407..5068cdb77 100644 --- a/modules/ui/src/app/store/reducers.ts +++ b/modules/ui/src/app/store/reducers.ts @@ -36,6 +36,10 @@ export const appComponentReducer = createReducer( on(Actions.updateFocusNavigation, (state, { focusNavigation }) => ({ ...state, focusNavigation, + })), + on(Actions.updateError, (state, { error }) => ({ + ...state, + error, })) ); diff --git a/modules/ui/src/app/store/selectors.spec.ts b/modules/ui/src/app/store/selectors.spec.ts index e1a25af9a..23b567d3f 100644 --- a/modules/ui/src/app/store/selectors.spec.ts +++ b/modules/ui/src/app/store/selectors.spec.ts @@ -16,6 +16,7 @@ import { AppState } from './state'; import { + selectError, selectHasConnectionSettings, selectInterfaces, selectMenuOpened, @@ -30,6 +31,7 @@ describe('Selectors', () => { isStatusLoaded: false, devicesLength: 0, focusNavigation: false, + error: false, }, settings: { systemConfig: {}, @@ -58,4 +60,9 @@ describe('Selectors', () => { 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 index 684f039e9..d93cc6eca 100644 --- a/modules/ui/src/app/store/selectors.ts +++ b/modules/ui/src/app/store/selectors.ts @@ -41,3 +41,8 @@ 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 index 300eb5ff8..196346bf6 100644 --- a/modules/ui/src/app/store/state.ts +++ b/modules/ui/src/app/store/state.ts @@ -32,6 +32,7 @@ export interface AppComponentState { * 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 } @@ -57,6 +58,7 @@ export const initialAppComponentState: AppComponentState = { focusNavigation: false, isStatusLoaded: false, devicesLength: 0, + error: false, }; export const initialSettingsState: SettingsState = { 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; }