Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions modules/ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ <h1 class="main-heading">Testrun</h1>
*ngIf="vm.hasConnectionSettings === true && vm.hasDevices === false">
Step 2: To perform a device test please
<a
(click)="navigateToDeviceRepository()"
(keydown.enter)="navigateToDeviceRepository()"
(keydown.space)="navigateToDeviceRepository()"
(click)="navigateToAddDevice()"
(keydown.enter)="navigateToAddDevice()"
(keydown.space)="navigateToAddDevice()"
aria-label="The Create a Device link redirects to the Devices page and opens the dialogue there."
tabindex="0"
role="link"
Expand Down Expand Up @@ -223,6 +223,29 @@ <h1 class="main-heading">Testrun</h1>
>Risk Assessment questionnaire</a
>?
</app-callout>
<app-callout
id="outdated_devices_callout"
role="alert"
aria-live="assertive"
[closed]="!!vm.calloutState.get('outdated_devices_callout')"
[closable]="true"
[type]="CalloutType.Error"
(calloutClosed)="calloutClosed($event)"
*ngIf="vm.hasExpiredDevices">
Further information is required in your device configurations. Please
update your
<a
(click)="navigateToDeviceRepository()"
(keydown.enter)="navigateToDeviceRepository()"
(keydown.space)="navigateToDeviceRepository()"
aria-label="The Devices link redirects to the device repository page."
tabindex="0"
role="link"
class="message-link"
>Devices</a
>
to continue testing.
</app-callout>
<router-outlet></router-outlet>
</div>
</mat-drawer-content>
Expand Down
27 changes: 27 additions & 0 deletions modules/ui/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
selectError,
selectHasConnectionSettings,
selectHasDevices,
selectHasExpiredDevices,
selectHasRiskProfiles,
selectInterfaces,
selectIsOpenStartTestrun,
Expand Down Expand Up @@ -164,6 +165,7 @@ describe('AppComponent', () => {
{ selector: selectError, value: null },
{ selector: selectMenuOpened, value: false },
{ selector: selectHasDevices, value: false },
{ selector: selectHasExpiredDevices, value: false },
{ selector: selectHasRiskProfiles, value: false },
{ selector: selectStatus, value: null },
{ selector: selectSystemStatus, value: null },
Expand Down Expand Up @@ -757,6 +759,31 @@ describe('AppComponent', () => {
});
});
});

describe('with expired devices', () => {
beforeEach(() => {
store.overrideSelector(selectHasExpiredDevices, true);
fixture.detectChanges();
});

it('should have callout component', () => {
const callouts = compiled.querySelectorAll('app-callout');
let hasExpiredDeviceCallout = false;
callouts.forEach(callout => {
if (
callout?.innerHTML
.trim()
.includes(
'Further information is required in your device configurations.'
)
) {
hasExpiredDeviceCallout = true;
}
});

expect(hasExpiredDeviceCallout).toBeTrue();
});
});
});

it('should not call toggleSettingsBtn focus on closeSetting when device length is 0', async () => {
Expand Down
9 changes: 9 additions & 0 deletions modules/ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export class AppComponent {

navigateToDeviceRepository(): void {
this.route.navigate([Routes.Devices]);
}
navigateToAddDevice(): void {
this.route.navigate([Routes.Devices]);
this.store.dispatch(setIsOpenAddDevice({ isOpenAddDevice: true }));
}

Expand Down Expand Up @@ -207,4 +210,10 @@ export class AppComponent {
this.appStore.setFocusOnPage();
});
}

calloutClosed(id: string | null) {
if (id) {
this.appStore.setCloseCallout(id);
}
}
}
29 changes: 28 additions & 1 deletion modules/ui/src/app/app.store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
*/
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { of, skip, take } from 'rxjs';
import { AppStore, CONSENT_SHOWN_KEY } from './app.store';
import { AppStore, CALLOUT_STATE_KEY, CONSENT_SHOWN_KEY } from './app.store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { AppState } from './store/state';
import {
selectError,
selectHasConnectionSettings,
selectHasDevices,
selectHasExpiredDevices,
selectHasRiskProfiles,
selectInterfaces,
selectIsOpenWaitSnackBar,
Expand Down Expand Up @@ -57,6 +58,12 @@ const mock = (() => {
setItem: (key: string, value: string) => {
store[key] = value + '';
},
getObject: (key: string) => {
return store[key] || null;
},
setObject: (key: string, value: object) => {
store[key] = JSON.stringify(value);
},
clear: () => {
store = {};
},
Expand Down Expand Up @@ -109,6 +116,7 @@ describe('AppStore', () => {
appStore = TestBed.inject(AppStore);

store.overrideSelector(selectHasDevices, true);
store.overrideSelector(selectHasExpiredDevices, true);
store.overrideSelector(selectHasRiskProfiles, false);
store.overrideSelector(selectReports, []);
store.overrideSelector(selectHasConnectionSettings, true);
Expand Down Expand Up @@ -154,6 +162,7 @@ describe('AppStore', () => {
expect(store).toEqual({
consentShown: false,
hasDevices: true,
hasExpiredDevices: true,
hasRiskProfiles: false,
reports: [],
isStatusLoaded: false,
Expand All @@ -162,6 +171,7 @@ describe('AppStore', () => {
isMenuOpen: true,
interfaces: {},
settingMissedError: null,
calloutState: new Map(),
});
done();
});
Expand Down Expand Up @@ -303,5 +313,22 @@ describe('AppStore', () => {
);
});
});

describe('setCloseCallout', () => {
it('should update store', done => {
appStore.viewModel$.pipe(skip(1), take(1)).subscribe(store => {
expect(store.calloutState.get('test')).toEqual(true);
done();
});

appStore.setCloseCallout('test');
});

it('should update storage', () => {
appStore.setCloseCallout('test');

expect(mock.getObject(CALLOUT_STATE_KEY)).toBeTruthy();
});
});
});
});
32 changes: 32 additions & 0 deletions modules/ui/src/app/app.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
selectError,
selectHasConnectionSettings,
selectHasDevices,
selectHasExpiredDevices,
selectHasRiskProfiles,
selectInterfaces,
selectMenuOpened,
Expand Down Expand Up @@ -52,16 +53,20 @@ import { TestRunMqttService } from './services/test-run-mqtt.service';
import { NotificationService } from './services/notification.service';

export const CONSENT_SHOWN_KEY = 'CONSENT_SHOWN';
export const CALLOUT_STATE_KEY = 'CALLOUT_STATE';
export interface AppComponentState {
consentShown: boolean;
isStatusLoaded: boolean;
systemStatus: TestrunStatus | null;
calloutState: Map<string, boolean>;
}
@Injectable()
export class AppStore extends ComponentStore<AppComponentState> {
private consentShown$ = this.select(state => state.consentShown);
private calloutState$ = this.select(state => state.calloutState);
private isStatusLoaded$ = this.select(state => state.isStatusLoaded);
private hasDevices$ = this.store.select(selectHasDevices);
private hasExpiredDevices$ = this.store.select(selectHasExpiredDevices);
private hasRiskProfiles$ = this.store.select(selectHasRiskProfiles);
private reports$ = this.store.select(selectReports);
private hasConnectionSetting$ = this.store.select(
Expand All @@ -77,6 +82,7 @@ export class AppStore extends ComponentStore<AppComponentState> {
viewModel$ = this.select({
consentShown: this.consentShown$,
hasDevices: this.hasDevices$,
hasExpiredDevices: this.hasExpiredDevices$,
hasRiskProfiles: this.hasRiskProfiles$,
reports: this.reports$,
isStatusLoaded: this.isStatusLoaded$,
Expand All @@ -85,13 +91,25 @@ export class AppStore extends ComponentStore<AppComponentState> {
isMenuOpen: this.isMenuOpen$,
interfaces: this.interfaces$,
settingMissedError: this.settingMissedError$,
calloutState: this.calloutState$,
});

updateConsent = this.updater((state, consentShown: boolean) => ({
...state,
consentShown,
}));

updateCalloutState = this.updater((state, callout: string) => {
const calloutState = state.calloutState;
calloutState.set(callout, true);
// @ts-expect-error property is defined in index.html
sessionStorage.setObject(CALLOUT_STATE_KEY, calloutState);
return {
...state,
calloutState: new Map(calloutState),
};
});

updateIsStatusLoaded = this.updater((state, isStatusLoaded: boolean) => ({
...state,
isStatusLoaded,
Expand Down Expand Up @@ -214,17 +232,31 @@ export class AppStore extends ComponentStore<AppComponentState> {
);
});

setCloseCallout = this.effect<string>(trigger$ => {
return trigger$.pipe(
tap((id: string) => {
this.updateCalloutState(id);
})
);
});

constructor(
private store: Store<AppState>,
private testRunService: TestRunService,
private testRunMqttService: TestRunMqttService,
private focusManagerService: FocusManagerService,
private notificationService: NotificationService
) {
// @ts-expect-error get object is defined in index.html
const calloutState = sessionStorage.getObject(CALLOUT_STATE_KEY);

super({
consentShown: sessionStorage.getItem(CONSENT_SHOWN_KEY) !== null,
isStatusLoaded: false,
systemStatus: null,
calloutState: calloutState
? new Map(Object.entries(calloutState))
: new Map(),
});
}
}
8 changes: 7 additions & 1 deletion modules/ui/src/app/components/callout/callout.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="callout-container" [ngClass]="type">
<div *ngIf="!closed" class="callout-container" [ngClass]="type">
<mat-icon
*ngIf="type !== CalloutType.InfoPilot"
class="callout-icon"
Expand All @@ -25,4 +25,10 @@
<p class="callout-context">
<ng-content></ng-content>
</p>
<button
class="callout-close-button"
mat-button
(click)="calloutClosed.emit(id)">
OK
</button>
</div>
10 changes: 10 additions & 0 deletions modules/ui/src/app/components/callout/callout.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
top: 60px;
}

:host.hidden {
display: none;
}

@media (width < 742px) {
:host + ::ng-deep app-callout {
top: 80px;
Expand Down Expand Up @@ -117,3 +121,9 @@
line-height: 20px;
letter-spacing: 0.2px;
}

.callout-close-button {
margin-left: auto;
margin-right: -20px;
color: $warn;
}
23 changes: 23 additions & 0 deletions modules/ui/src/app/components/callout/callout.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,27 @@ describe('CalloutComponent', () => {

expect(calloutContainerdEl?.classList).toContain('mockValue');
});

describe('closeable', () => {
beforeEach(() => {
component.closable = true;
fixture.detectChanges();
});

it('should have close button', () => {
const closeButton = compiled.querySelector('.callout-close-button');

expect(closeButton).toBeTruthy();
});

it('should emit event', () => {
const calloutClosedSpy = spyOn(component.calloutClosed, 'emit');
const closeButton = compiled.querySelector(
'.callout-close-button'
) as HTMLButtonElement;
closeButton?.click();

expect(calloutClosedSpy).toHaveBeenCalled();
});
});
});
16 changes: 14 additions & 2 deletions modules/ui/src/app/components/callout/callout.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
HostBinding,
Input,
Output,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { CalloutType } from '../../model/callout-type';

@Component({
selector: 'app-callout',
standalone: true,
imports: [CommonModule, MatIconModule],
imports: [CommonModule, MatIconModule, MatButtonModule],
templateUrl: './callout.component.html',
styleUrls: ['./callout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CalloutComponent {
public readonly CalloutType = CalloutType;
@HostBinding('class.hidden') @Input() closed: boolean = false;
@Input() id: string | null = null;
@Input() type = '';
@Input() closable = false;
@Output() calloutClosed = new EventEmitter<string | null>();
}
Loading