diff --git a/src/app/group-monitor/backend.service.ts b/src/app/group-monitor/backend.service.ts index 8d240b760c7c77b392c3342012a5c87b6f0a9dc5..f9b6d84f511acb070de789f82559817bc026191a 100644 --- a/src/app/group-monitor/backend.service.ts +++ b/src/app/group-monitor/backend.service.ts @@ -75,4 +75,19 @@ export class BackendService extends WebsocketBackendService<TestSession[]> { ) .subscribe(); } + + unlock(group_name: string, testIds: number[]): Subscription { + + return this.http + .post(this.serverUrl + `monitor/group/${group_name}/tests/unlock`, {testIds}) + .pipe( + catchError(() => { + // TODO interceptor should have interfered and moved to error-page ... + // https://github.com/iqb-berlin/testcenter-frontend/issues/53 + console.warn(`unlocking failed: command`, testIds); + return of(false); + }) + ) + .subscribe(); + } } diff --git a/src/app/group-monitor/group-monitor.component.css b/src/app/group-monitor/group-monitor.component.css index a12ba8ca97ca24d1f84be072178268db53a96722..97e8d9d93992d6ff342194b1fc084b8fabb86b89 100644 --- a/src/app/group-monitor/group-monitor.component.css +++ b/src/app/group-monitor/group-monitor.component.css @@ -51,7 +51,7 @@ } .connection-status.error { - background: red + background: #821123 } .connection-status.ws-offline { @@ -164,10 +164,10 @@ mat-sidenav { } .hide-scroll-hint .scroll-hint { - animation: fade 0.3s reverse forwards; + animation: fade-and-shrink 0.3s reverse forwards; } -@keyframes fade { +@keyframes fade-and-shrink { 0% { opacity: 0; transform: scale(0) @@ -200,11 +200,32 @@ mat-sidenav { text-transform: uppercase; } +.alert-warning { + border-radius: 4px; + box-sizing: border-box; + margin: 0 0 0.5em 0; + width: 100%; + padding: 14px 16px; + min-height: 48px; + color: hsla(0,0%,100%,.7); + background: #821123; + display: inline-flex; + vertical-align: middle; + align-items: center; + animation: fade 30s reverse forwards; +} +.alert-warning mat-icon { + margin-right: 4px; +} +@keyframes fade { + 0% { + opacity: 0; + } - - - - + 100% { + opacity: 1; + } +} diff --git a/src/app/group-monitor/group-monitor.component.html b/src/app/group-monitor/group-monitor.component.html index 35dbb5f22c086a25b1bd03da26a638a4833ac071..3f4645ac8add0557513ed5a8a1a57fea6ed124d9 100644 --- a/src/app/group-monitor/group-monitor.component.html +++ b/src/app/group-monitor/group-monitor.component.html @@ -99,6 +99,12 @@ </button> </div> + <div class="toolbar-section"> + <button mat-raised-button class="control" color="primary" (click)="testCommandUnlock()" [disabled]="!isUnlockAllowed()"> + <mat-icon>lock-open</mat-icon>ENTSPERREN + </button> + </div> + <hr> <div class="toolbar-section"> @@ -108,6 +114,12 @@ <mat-radio-button value="unit" disabled>Aufgabe</mat-radio-button> </mat-radio-group> </div> + + <div class="toolbar-section"> + <div *ngFor="let warning of warnings | keyvalue" class="alert-warning"> + <mat-icon>warning</mat-icon>{{warning.value.text}} + </div> + </div> </mat-sidenav> <mat-sidenav-content> @@ -172,10 +184,7 @@ </table> </div> </div> - </mat-sidenav-content> - - </mat-sidenav-container> <button diff --git a/src/app/group-monitor/group-monitor.component.ts b/src/app/group-monitor/group-monitor.component.ts index 67178830f529be5e10d646fb142a5660a6456c29..4ec4f716722fe88bd793e4e7d5525ee9c0370e03 100644 --- a/src/app/group-monitor/group-monitor.component.ts +++ b/src/app/group-monitor/group-monitor.component.ts @@ -31,6 +31,7 @@ export class GroupMonitorComponent implements OnInit, OnDestroy { ) {} ownGroup$: Observable<GroupData>; + private ownGroupName: string = ''; monitor$: Observable<TestSession[]>; connectionStatus$: Observable<ConnectionStatus>; @@ -80,12 +81,14 @@ export class GroupMonitorComponent implements OnInit, OnDestroy { sessionCheckedGroupCount: number; isScrollable = false; - @ViewChild('adminbackground') mainElem:ElementRef; - private routingSubscription: Subscription = null; + warnings: {[key: string]: {text: string, timeout: number}} = {}; + @ViewChild('adminbackground') mainElem:ElementRef; @ViewChild('sidenav', {static: true}) sidenav: MatSidenav; + private routingSubscription: Subscription = null; + static getFirstUnit(testletOrUnit: Testlet|Unit): Unit|null { while (!isUnit(testletOrUnit)) { if (!testletOrUnit.children.length) { @@ -107,6 +110,7 @@ export class GroupMonitorComponent implements OnInit, OnDestroy { ngOnInit(): void { this.routingSubscription = this.route.params.subscribe(params => { this.ownGroup$ = this.bs.getGroupData(params['group-name']); + this.ownGroupName = params['group-name']; }); this.sortBy$ = new BehaviorSubject<Sort>({direction: 'asc', active: 'personLabel'}); @@ -118,8 +122,8 @@ export class GroupMonitorComponent implements OnInit, OnDestroy { combineLatest<[Sort, TestSessionFilter[], TestSession[]]>([this.sortBy$, this.filters$, this.monitor$]) .pipe( - map(([sortBy, filters, sessions]) => this.sortSessions(sortBy, this.filterSessions(sessions, filters))), - tap(sessions => this.updateChecked(sessions)) + map(([sortBy, filters, sessions]) => this.sortSessions(sortBy, this.filterSessions(sessions, filters))), + tap(sessions => this.updateChecked(sessions)), ) .subscribe(this.sessions$); @@ -230,16 +234,40 @@ export class GroupMonitorComponent implements OnInit, OnDestroy { this.bs.command('pause', [], testIds); } - testCommandGoto() { + testCommandGoto(): void { if ((this.sessionCheckedGroupCount === 1) && (Object.keys(this.checkedSessions).length > 0)) { const testIds = Object.values(this.checkedSessions) - .filter((session) => session.testId && session.testId > -1) - .map((session) => session.testId); + .filter(session => session.testId && session.testId > -1) + .map(session => session.testId); this.bs.command('goto', ['id', GroupMonitorComponent.getFirstUnit(this.selectedElement.element).id], testIds); } } - selectElement(selected: Selected) { + testCommandUnlock(): void { + const sessions = Object.values(this.checkedSessions) + .filter(session => GroupMonitorComponent.hasState(session.testState, 'status', 'locked')) + this.bs.unlock(this.ownGroupName, sessions.map(session => session.testId)).add(() => { + const plural = sessions.length > 1; + this.addWarning('reload-some-clients', + `${plural ? sessions.length : 'Ein'} Test${plural ? 's': ''} + wurde${plural ? 'n': ''} entsperrt. ${plural ? 'Die': 'Der'} Teilnehmer + ${plural ? 'müssen': 'muss'} die Webseite aufrufen bzw. neuladen, + damit ${plural ? 'die': 'der'} Test${plural ? 's': ''} wieder aufgenommen werden kann!` + ); + }); + } + + private addWarning(key, text): void { + if (typeof this.warnings[key] !== "undefined") { + window.clearTimeout(this.warnings[key].timeout); + } + this.warnings[key] = { + text, + timeout: window.setTimeout(() => delete this.warnings[key], 30000) + } + } + + selectElement(selected: Selected): void { this.selectedElement = selected; let toCheck: TestSession[] = []; if (selected.element) { @@ -332,30 +360,42 @@ export class GroupMonitorComponent implements OnInit, OnDestroy { const activeSessions = Object.values(this.checkedSessions).length && Object.values(this.checkedSessions) .filter((session) => GroupMonitorComponent.hasState(session.testState, 'status', 'running')); return activeSessions.length && activeSessions - .filter(session => GroupMonitorComponent.hasState(session.testState, 'status', 'running')) - .filter(session => GroupMonitorComponent.hasState(session.testState, 'CONTROLLER', 'PAUSED')) - .length === 0; + .filter(session => GroupMonitorComponent.hasState(session.testState, 'status', 'running')) + .filter(session => GroupMonitorComponent.hasState(session.testState, 'CONTROLLER', 'PAUSED')) + .length === 0; } - isResumeAllowed() { + isResumeAllowed(): boolean { const activeSessions = Object.values(this.checkedSessions) - .filter((session) => GroupMonitorComponent.hasState(session.testState, 'status', 'running')); + .filter((session) => GroupMonitorComponent.hasState(session.testState, 'status', 'running')); return activeSessions.length && activeSessions - .filter((session) => !GroupMonitorComponent.hasState(session.testState, 'CONTROLLER', 'PAUSED')) - .length === 0; + .filter((session) => !GroupMonitorComponent.hasState(session.testState, 'CONTROLLER', 'PAUSED')) + .length === 0; + } + + isUnlockAllowed(): boolean { + const lockedSessions = Object.values(this.checkedSessions) + .filter(session => GroupMonitorComponent.hasState(session.testState, 'status', 'locked')); + return lockedSessions.length && (lockedSessions.length === Object.values(this.checkedSessions).length); } - ngAfterViewChecked() { + ngAfterViewChecked(): void { this.isScrollable = this.mainElem.nativeElement.clientHeight < this.mainElem.nativeElement.scrollHeight; } - scrollDown() { + scrollDown(): void { this.mainElem.nativeElement.scrollTo(0, this.mainElem.nativeElement.scrollHeight); } - updateScrollHint() { + updateScrollHint(): void { const elem = this.mainElem.nativeElement; const reachedBottom = (elem.scrollTop + elem.clientHeight === elem.scrollHeight); elem.classList[reachedBottom ? 'add' : 'remove']('hide-scroll-hint'); } + + showClientsMustBeReloadedWarning(): boolean { + return true; + // return this.sessionsMustBeReloaded && this.sessions$.getValue() + // .filter(session => this.sessionsMustBeReloaded.indexOf(session.testId)) // STAND sessiosn filtern + } } diff --git a/src/app/group-monitor/test-view/test-view.component.css b/src/app/group-monitor/test-view/test-view.component.css index 0c1b2be9a239e445b62360e4afcf5d01a5438579..40f8a728d65b9febec130aa588482104db2afc6c 100644 --- a/src/app/group-monitor/test-view/test-view.component.css +++ b/src/app/group-monitor/test-view/test-view.component.css @@ -24,10 +24,6 @@ mat-icon + h1 { transform-style: preserve-3d; } -.error .units { - background: rgba(255, 0, 0, 0.3); -} - .units:before { background: #003333; /*width: 100%;*/ @@ -156,12 +152,12 @@ mat-icon + h1 { } .warning { - color: red; + color: #821123; font-weight: bold } .unit-badge.danger { - color: red; + color: #821123; } .unit-badge.success { color: #b2ff59 diff --git a/src/app/group-monitor/test-view/test-view.component.ts b/src/app/group-monitor/test-view/test-view.component.ts index 2729debd64c455dc26891793dc75c952779bce69..76cb76c0bfbdddab5624bc44c9ab1279e625bb36 100644 --- a/src/app/group-monitor/test-view/test-view.component.ts +++ b/src/app/group-monitor/test-view/test-view.component.ts @@ -112,9 +112,13 @@ export class TestViewComponent implements OnInit, OnChanges, OnDestroy { if (this.hasState(state, 'status', 'locked')) { return {tooltip: 'Test gesperrt', icon: 'lock_closed'} } - if (this.hasState(state, 'CONTROLLER', 'error')) { + if (this.hasState(state, 'CONTROLLER', 'ERROR')) { return {tooltip: 'Es ist ein Fehler aufgetreten!', icon: 'error', class: 'danger'} } + if (this.hasState(state, 'CONTROLLER', 'TERMINATED')) { + return {tooltip: 'Testausführung wurde beendet und kann wieder aufgenommen werden. ' + + 'Der Browser des Teilnehmers muss ggf. neu geladen werden!', icon: 'warning', class: 'danger'} + } if (this.hasState(state, 'CONNECTION', 'LOST')) { return {tooltip: 'Seite wurde verlassen oder Browserfenster geschlossen!', icon: 'error', class: 'danger'} }