File

src/app/group-monitor/group-monitor.component.ts

Implements

OnInit OnDestroy

Metadata

selector app-group-monitor
styleUrls ./group-monitor.component.css
templateUrl ./group-monitor.component.html

Index

Properties
Methods

Constructor

constructor(dialog: MatDialog, route: ActivatedRoute, bs: BackendService, tsm: TestSessionManager, router: Router, cts: CustomtextService, mds: MainDataService)
Parameters :
Name Type Optional
dialog MatDialog No
route ActivatedRoute No
bs BackendService No
tsm TestSessionManager No
router Router No
cts CustomtextService No
mds MainDataService No

Methods

Private commandResponseToMessage
commandResponseToMessage(commandResponse: CommandResponse)
Parameters :
Name Type Optional
commandResponse CommandResponse No
Returns : UIMessage
finishEverythingCommand
finishEverythingCommand()
Returns : void
getSessionColor
getSessionColor(session: TestSession)
Parameters :
Name Type Optional
session TestSession No
Returns : string
invertChecked
invertChecked(event: Event)
Parameters :
Name Type Optional
event Event No
Returns : boolean
markElement
markElement(marking: Selected)
Parameters :
Name Type Optional
marking Selected No
Returns : void
ngAfterViewChecked
ngAfterViewChecked()
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Private onCheckedChange
onCheckedChange(stats: TestSessionSetStats)
Parameters :
Name Type Optional
stats TestSessionSetStats No
Returns : void
Private onSessionsUpdate
onSessionsUpdate(stats: TestSessionSetStats)
Parameters :
Name Type Optional
stats TestSessionSetStats No
Returns : void
scrollDown
scrollDown()
Returns : void
selectElement
selectElement(selected: Selected)
Parameters :
Name Type Optional
selected Selected No
Returns : void
Private selectNextBlock
selectNextBlock()
Returns : void
setDisplayOption
setDisplayOption(option: string, value)
Parameters :
Name Type Optional
option string No
value No
Returns : void
setTableSorting
setTableSorting(sort: Sort)
Parameters :
Name Type Optional
sort Sort No
Returns : void
testCommandGoto
testCommandGoto()
Returns : void
toggleAlwaysCheckAll
toggleAlwaysCheckAll(event: MatSlideToggleChange)
Parameters :
Name Type Optional
event MatSlideToggleChange No
Returns : void
toggleCheckAll
toggleCheckAll(event: MatCheckboxChange)
Parameters :
Name Type Optional
event MatCheckboxChange No
Returns : void
toggleChecked
toggleChecked(checked: boolean, session: TestSession)
Parameters :
Name Type Optional
checked boolean No
session TestSession No
Returns : void
unlockCommand
unlockCommand()
Returns : void
updateScrollHint
updateScrollHint()
Returns : void

Properties

connectionStatus$
Type : Observable<ConnectionStatus>
Public dialog
Type : MatDialog
displayOptions
Type : TestViewDisplayOptions
Default value : { view: 'medium', groupColumn: 'hide', bookletColumn: 'show', blockColumn: 'show', unitColumn: 'hide', highlightSpecies: false, manualChecking: false }
isClosing
Default value : false
isScrollable
Default value : false
mainElem
Type : ElementRef
Decorators :
@ViewChild('adminbackground')
markedElement
Type : Selected
Public mds
Type : MainDataService
messages
Type : UIMessage[]
Default value : []
ownGroup$
Type : Observable<GroupData>
Private ownGroupName
Type : string
Default value : ''
selectedElement
Type : Selected
sidenav
Type : MatSidenav
Decorators :
@ViewChild('sidenav', {static: true})
Private subscriptions
Type : Subscription[]
Default value : []
trackSession
Default value : () => {...}
Public tsm
Type : TestSessionManager
import {
  Component, ElementRef, OnDestroy, OnInit, ViewChild
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Sort } from '@angular/material/sort';
import { MatSidenav } from '@angular/material/sidenav';
import { interval, Observable, Subscription } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent, ConfirmDialogData, CustomtextService } from 'iqb-components';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { switchMap } from 'rxjs/operators';
import { BackendService } from './backend.service';
import {
  GroupData,
  TestViewDisplayOptions,
  TestViewDisplayOptionKey, Selected, TestSession, TestSessionSetStats, CommandResponse, UIMessage, isBooklet
} from './group-monitor.interfaces';
import { TestSessionManager } from './test-session-manager/test-session-manager.service';
import { ConnectionStatus } from '../shared/websocket-backend.service';
import { BookletUtil } from './booklet/booklet.util';
import { MainDataService } from '../maindata.service';

@Component({
  selector: 'app-group-monitor',
  templateUrl: './group-monitor.component.html',
  styleUrls: ['./group-monitor.component.css']
})
export class GroupMonitorComponent implements OnInit, OnDestroy {
  connectionStatus$: Observable<ConnectionStatus>;

  ownGroup$: Observable<GroupData>;
  private ownGroupName = '';

  selectedElement: Selected;
  markedElement: Selected;

  displayOptions: TestViewDisplayOptions = {
    view: 'medium',
    groupColumn: 'hide',
    bookletColumn: 'show',
    blockColumn: 'show',
    unitColumn: 'hide',
    highlightSpecies: false,
    manualChecking: false
  };

  isScrollable = false;
  isClosing = false;

  messages: UIMessage[] = [];

  private subscriptions: Subscription[] = [];

  @ViewChild('adminbackground') mainElem: ElementRef;
  @ViewChild('sidenav', { static: true }) sidenav: MatSidenav;

  constructor(
    public dialog: MatDialog,
    private route: ActivatedRoute,
    private bs: BackendService,
    public tsm: TestSessionManager,
    private router: Router,
    private cts: CustomtextService,
    public mds: MainDataService
  ) {}

  ngOnInit(): void {
    this.subscriptions = [
      this.route.params.subscribe(params => {
        this.ownGroup$ = this.bs.getGroupData(params['group-name']);
        this.ownGroupName = params['group-name'];
        this.tsm.connect(params['group-name']);
      }),
      this.tsm.sessionsStats$.subscribe(stats => {
        this.onSessionsUpdate(stats);
      }),
      this.tsm.checkedStats$.subscribe(stats => {
        this.onCheckedChange(stats);
      }),
      this.tsm.commandResponses$.subscribe(commandResponse => {
        this.messages.push(this.commandResponseToMessage(commandResponse));
      }),
      this.tsm.commandResponses$
        .pipe(switchMap(() => interval(7000)))
        .subscribe(() => this.messages.shift())
    ];

    this.connectionStatus$ = this.bs.connectionStatus$;
    this.mds.appSubTitle$.next(this.cts.getCustomText('gm_headline'));
  }

  private commandResponseToMessage(commandResponse: CommandResponse): UIMessage {
    const command = this.cts.getCustomText(`gm_control_${commandResponse.commandType}`) || commandResponse.commandType;
    const successWarning = this.cts.getCustomText(`gm_control_${commandResponse.commandType}_success_warning`) || '';
    if (!commandResponse.testIds.length) {
      return {
        level: 'warning',
        text: 'Keine Tests Betroffen von: `%s`',
        customtext: 'gm_message_no_session_affected_by_command',
        replacements: [command, commandResponse.testIds.length.toString(10)]
      };
    }
    return {
      level: successWarning ? 'warning' : 'info',
      text: '`%s` an `%s` tests gesendet! %s',
      customtext: 'gm_message_command_sent_n_sessions',
      replacements: [command, commandResponse.testIds.length.toString(10), successWarning]
    };
  }

  ngOnDestroy(): void {
    this.tsm.disconnect();
    this.subscriptions.forEach(subscription => subscription.unsubscribe());
  }

  ngAfterViewChecked(): void {
    this.isScrollable = this.mainElem.nativeElement.clientHeight < this.mainElem.nativeElement.scrollHeight;
  }

  private onSessionsUpdate(stats: TestSessionSetStats): void {
    this.displayOptions.highlightSpecies = (stats.differentBookletSpecies > 1);

    if (!this.tsm.checkingOptions.enableAutoCheckAll) {
      this.displayOptions.manualChecking = true;
    }
  }

  private onCheckedChange(stats: TestSessionSetStats): void {
    if (stats.differentBookletSpecies > 1) {
      this.selectedElement = null;
    }
  }

  trackSession = (index: number, session: TestSession): number => session.data.testId;

  setTableSorting(sort: Sort): void {
    if (!sort.active || sort.direction === '') {
      return;
    }
    this.tsm.sortBy$.next(sort);
  }

  setDisplayOption(option: string, value: TestViewDisplayOptions[TestViewDisplayOptionKey]): void {
    this.displayOptions[option] = value;
  }

  scrollDown(): void {
    this.mainElem.nativeElement.scrollTo(0, this.mainElem.nativeElement.scrollHeight);
  }

  updateScrollHint(): void {
    const elem = this.mainElem.nativeElement;
    const reachedBottom = (elem.scrollTop + elem.clientHeight === elem.scrollHeight);
    elem.classList[reachedBottom ? 'add' : 'remove']('hide-scroll-hint');
  }

  getSessionColor(session: TestSession): string {
    const stripes = (c1, c2) => `repeating-linear-gradient(45deg, ${c1}, ${c1} 10px, ${c2} 10px, ${c2} 20px)`;
    const hsl = (h, s, l) => `hsl(${h}, ${s}%, ${l}%)`;
    const colorful = this.displayOptions.highlightSpecies && session.booklet.species;
    const h = colorful ? (
      session.booklet.species.length *
      session.booklet.species.charCodeAt(0) *
      session.booklet.species.charCodeAt(session.booklet.species.length / 4) *
      session.booklet.species.charCodeAt(session.booklet.species.length / 4) *
      session.booklet.species.charCodeAt(session.booklet.species.length / 2) *
      session.booklet.species.charCodeAt(3 * (session.booklet.species.length / 4)) *
      session.booklet.species.charCodeAt(session.booklet.species.length - 1)
    ) % 360 : 0;

    switch (session.state) {
      case 'paused':
        return hsl(h, colorful ? 45 : 0, 90);
      case 'pending':
        return stripes(hsl(h, colorful ? 75 : 0, 95), hsl(h, 0, 98));
      case 'locked':
        return stripes(hsl(h, colorful ? 75 : 0, 95), hsl(0, 0, 92));
      case 'error':
        return stripes(hsl(h, colorful ? 75 : 0, 95), hsl(0, 30, 95));
      default:
        return hsl(h, colorful ? 75 : 0, colorful ? 95 : 100);
    }
  }

  markElement(marking: Selected): void {
    this.markedElement = marking;
  }

  selectElement(selected: Selected): void {
    this.tsm.checkSessionsBySelection(selected);
    this.selectedElement = selected;
  }

  finishEverythingCommand(): void {
    const dialogRef = this.dialog.open(ConfirmDialogComponent, {
      width: 'auto',
      data: <ConfirmDialogData>{
        title: 'Testdurchführung Beenden',
        content: 'Achtung! Diese Aktion sperrt und beendet sämtliche Tests dieser Sitzung.',
        confirmbuttonlabel: 'Ja, ich möchte die Testdurchführung Beenden',
        showcancel: true
      }
    });

    dialogRef.afterClosed().subscribe((confirmed: boolean) => {
      if (confirmed) {
        this.isClosing = true;
        this.tsm.commandFinishEverything()
          .subscribe(() => {
            setTimeout(() => { this.router.navigateByUrl('/r/login'); }, 5000); // go away
          });
      }
    });
  }

  testCommandGoto(): void {
    if (!this.selectedElement?.element?.blockId) {
      this.messages.push({
        level: 'warning',
        customtext: 'gm_test_command_no_selected_block',
        text: 'Kein Zielblock ausgewählt'
      });
    } else {
      this.tsm.testCommandGoto(this.selectedElement)
        .subscribe(() => this.selectNextBlock());
    }
  }

  private selectNextBlock(): void {
    if (!isBooklet(this.selectedElement.originSession.booklet)) {
      return;
    }
    this.selectedElement = {
      element: this.selectedElement.element.nextBlockId ?
        BookletUtil.getBlockById(
          this.selectedElement.element.nextBlockId,
          this.selectedElement.originSession.booklet
        ) : null,
      inversion: false,
      originSession: this.selectedElement.originSession,
      spreading: this.selectedElement.spreading
    };
  }

  unlockCommand(): void {
    this.tsm.testCommandUnlock();
  }

  toggleChecked(checked: boolean, session: TestSession): void {
    if (!this.tsm.isChecked(session)) {
      this.tsm.checkSession(session);
    } else {
      this.tsm.uncheckSession(session);
    }
  }

  invertChecked(event: Event): boolean {
    event.preventDefault();
    this.tsm.invertChecked();
    return false;
  }

  toggleAlwaysCheckAll(event: MatSlideToggleChange): void {
    if (this.tsm.checkingOptions.enableAutoCheckAll && event.checked) {
      this.tsm.checkAll();
      this.displayOptions.manualChecking = false;
      this.tsm.checkingOptions.autoCheckAll = true;
    } else {
      this.tsm.checkNone();
      this.displayOptions.manualChecking = true;
      this.tsm.checkingOptions.autoCheckAll = false;
    }
  }

  toggleCheckAll(event: MatCheckboxChange): void {
    if (event.checked) {
      this.tsm.checkAll();
    } else {
      this.tsm.checkNone();
    }
  }
}
<div class="page-header">
  <p>
    {{mds.appTitle$ | async}} {{mds.appSubTitle$ | async}} -
    <span *ngIf="ownGroup$ | async as ownGroup">{{ownGroup.label}}</span>
  </p>
  <span class="fill-remaining-space"></span>
  <p>
    <mat-chip-list *ngIf="connectionStatus$ | async as connectionStatus">

      <mat-chip [class]="connectionStatus + ' connection-status'">
        <mat-icon>
          {{connectionStatus === 'error' ? 'error' : ''}}
          {{connectionStatus === 'polling-fetch' ? 'loop' : ''}}
          {{connectionStatus === 'polling-sleep' ? 'loop' : ''}}
          {{connectionStatus === 'ws-offline' ? 'loop' : ''}}
          {{connectionStatus === 'ws-online' ? 'wifi_tethering' : ''}}
        </mat-icon>
        {{connectionStatus === 'error' ? 'Offline' : ''}}
        {{connectionStatus === 'polling-fetch' ? 'Online' : ''}}
        {{connectionStatus === 'polling-sleep' ? 'Online' : ''}}
        {{connectionStatus === 'ws-offline' ? 'Reconn.' : ''}}
        {{connectionStatus === 'ws-online' ? 'Live' : ''}}
      </mat-chip>
    </mat-chip-list>
  </p>
</div>

<mat-menu #rootMenu="matMenu">
  <button mat-menu-item [matMenuTriggerFor]="filters">
    {{'Sitzungen ausblenden' | customtext:'gm_menu_filter' | async}}
  </button>
  <button mat-menu-item [matMenuTriggerFor]="group">
    {{'Spalten' | customtext:'gm_menu_cols' | async}}
  </button>
  <button mat-menu-item [matMenuTriggerFor]="activity">
    {{'Aktivität' | customtext:'gm_menu_activity' | async}}
  </button>
</mat-menu>

<mat-menu #filters="matMenu">
  <button mat-menu-item *ngFor="let filterOption of tsm.filterOptions; let i = index" (click)="tsm.switchFilter(i)">
    <mat-icon *ngIf="filterOption.selected">check</mat-icon>
    <span>{{filterOption.label | customtext:filterOption.label | async}}</span>
  </button>
</mat-menu>

<mat-menu #group="matMenu">
  <button mat-menu-item (click)="setDisplayOption('groupColumn', (displayOptions.groupColumn === 'hide') ? 'show' : 'hide')">
    <mat-icon *ngIf="displayOptions.groupColumn === 'show'">check</mat-icon>
    <span>{{'Gruppe' | customtext:'gm_col_group' | async}}</span>
  </button>
  <button mat-menu-item (click)="setDisplayOption('bookletColumn', (displayOptions.bookletColumn === 'hide') ? 'show' : 'hide')">
    <mat-icon *ngIf="displayOptions.bookletColumn === 'show'">check</mat-icon>
    <span>{{'Testheft' | customtext:'gm_col_booklet' | async}}</span>
  </button>
  <button mat-menu-item (click)="setDisplayOption('blockColumn', (displayOptions.blockColumn === 'hide') ? 'show' : 'hide')">
    <mat-icon *ngIf="displayOptions.blockColumn === 'show'">check</mat-icon>
    <span>{{'Block' | customtext:'gm_col_testlet' | async}}</span>
  </button>
  <button mat-menu-item (click)="setDisplayOption('unitColumn', (displayOptions.unitColumn === 'hide') ? 'show' : 'hide')">
    <mat-icon *ngIf="displayOptions.unitColumn === 'show'">check</mat-icon>
    <span>{{'Aufgabe' | customtext:'gm_col_unit' | async}}</span>
  </button>
</mat-menu>

<mat-menu #activity="matMenu">
  <button mat-menu-item (click)="setDisplayOption('view', 'full')">
    <mat-icon *ngIf="displayOptions.view === 'full'">check</mat-icon>
    <span>{{'Vollständig' | customtext:'gm_view_full' | async}}</span>
  </button>
  <button mat-menu-item (click)="setDisplayOption('view', 'medium')">
    <mat-icon *ngIf="displayOptions.view === 'medium'">check</mat-icon>
    <span>{{'Nur Blöcke' | customtext:'gm_view_medium' | async}}</span>
  </button>
  <button mat-menu-item (click)="setDisplayOption('view', 'small')">
    <mat-icon *ngIf="displayOptions.view === 'small'">check</mat-icon>
    <span>{{'Kurz' | customtext:'gm_view_small' | async}}</span>
  </button>
</mat-menu>

<div class="page-body">

  <mat-sidenav-container>
    <mat-sidenav #sidenav opened="true" mode="side" class="toolbar" fixedInViewport="true" fixedTopGap="55">

      <h2>{{'Test-Steuerung' | customtext:'gm_controls' | async}}</h2>

      <div class="toolbar-section" *ngIf="tsm.sessionsStats$ | async as sessionsStats">
        <mat-slide-toggle
            color="accent"
            (change)="toggleAlwaysCheckAll($event)"
            [disabled]="!tsm.checkingOptions.enableAutoCheckAll"
            [checked]="tsm.checkingOptions.autoCheckAll"
            [matTooltip]="(sessionsStats.differentBookletSpecies > 1) ? (
            'Die verwendeten Booklets sind zu unterschiedlich, um gemeinsam gesteuert zu werden.'
              | customtext:'gm_multiple_booklet_species_warning'
              | async
           ) : null"
        >
          {{'Alle Tests gleichzeitig steuern' | customtext:'gm_auto_checkall' | async }}
        </mat-slide-toggle>
      </div>

      <div class="toolbar-section min-height-section">
        <ng-container *ngIf="displayOptions.manualChecking">
          <ng-container *ngIf="tsm.checkedStats$ | async as checkedStats">
            <alert
              *ngIf="checkedStats.number; else noCheckedSession"
              level="info"
              customtext="gm_selection_info"
              text="%s %s Test%s mit %s Testheft%s ausgewählt."
              [replacements]="[
                (checkedStats.all ? ' Alle' : ''),
                checkedStats.number.toString(10),
                (checkedStats.number !== 1 ? 's' : ''),
                checkedStats.differentBooklets.toString(10),
                (checkedStats.differentBooklets !== 1 ? 'en' : '')
              ]"
            ></alert>
            <ng-template #noCheckedSession>
              <alert level="info" customtext="gm_selection_info_none" text="Kein Test ausgewählt."></alert>
            </ng-template>
          </ng-container>
        </ng-container>
      </div>

      <div class="toolbar-section">
        <button mat-raised-button class="control" color="primary" (click)="tsm.testCommandResume()">
          <mat-icon>play_arrow</mat-icon>
          {{'weiter' | customtext:'gm_control_resume' | async}}
        </button>

        <button mat-raised-button class="control" color="primary" (click)="tsm.testCommandPause()">
          <mat-icon>pause</mat-icon>
          {{'pause' | customtext:'gm_control_pause' | async}}
        </button>
      </div>

      <div class="toolbar-section">
        <button
          mat-raised-button
          class="control"
          color="primary"
          (click)="testCommandGoto()"
          [matTooltip]="selectedElement?.element?.blockId ? null : ('Bitte Block auswählen' | customtext:'gm_control_goto_tooltip' | async)"
        >
          <mat-icon>arrow_forward</mat-icon>
          {{'Springe zu' | customtext:'gm_control_goto' | async}}
          <span class="emph">{{selectedElement?.element?.blockId}}</span>
        </button>
      </div>

      <div class="toolbar-section">
        <button
          mat-raised-button
          class="control"
          color="primary"
          (click)="unlockCommand()"
          [matTooltip]="'Freigeben' | customtext:'gm_control_unlock_tooltip' | async"
        >
          <mat-icon>lock_open</mat-icon>
          {{'Test Entsperren' | customtext:'gm_control_unlock' | async}}
        </button>
      </div>

      <div id="message-panel" class="toolbar-section">
        <alert
          *ngFor="let m of messages"
          [text]="m.text"
          [level]="m.level"
          customtext="m.customtext"
          [replacements]="m.replacements"
        ></alert>
      </div>

      <div class="toolbar-section toolbar-section-bottom">
        <button mat-raised-button class="control" color="primary" (click)="finishEverythingCommand()">
          <mat-icon>stop</mat-icon>{{'Testung beenden' | customtext:'gm_control_finish_everything' | async}}
        </button>
      </div>

    </mat-sidenav>

    <mat-sidenav-content>

      <div #adminbackground class="adminbackground" (scroll)="updateScrollHint()">

        <div class="corner-menu">
          <button
            class="settings-button"
            mat-icon-button
            [matMenuTriggerFor]="rootMenu"
            [matTooltip]="'Ansicht' | customtext:'gm_settings_tooltip' | async"
            matTooltipPosition="above"
          >
            <mat-icon>settings</mat-icon>
          </button>
        </div>

        <div class="scroll-hint" *ngIf="isScrollable">
          <button
            mat-icon-button
            (click)="scrollDown()"
            [matTooltip]="'Ganz nach unten' | customtext:'gm_scroll_down' | async"
            matTooltipPosition="above"
          >
            <mat-icon>keyboard_arrow_down</mat-icon>
          </button>
        </div>

        <div class="test-session-table-wrapper">
          <table class="test-session-table" matSort (matSortChange)="setTableSorting($event)">
            <thead>
              <tr class="mat-sort-container">
                <td mat-sort-header="_checked" *ngIf="displayOptions.manualChecking">
                  <mat-checkbox
                    *ngIf="tsm.checkedStats$ | async as checkedStats"
                    (click)="$event.stopPropagation()"
                    (change)="toggleCheckAll($event)"
                    [checked]="checkedStats.all"
                    (contextmenu)="invertChecked($event)"
                  ></mat-checkbox>
                </td>
                <td mat-sort-header="_superState">
                  <mat-icon>person</mat-icon>
                </td>
                <td mat-sort-header="groupLabel" *ngIf="displayOptions.groupColumn === 'show'">
                  {{'Gruppe' | customtext:'gm_col_group' | async}}
                </td>
                <td mat-sort-header="personLabel">
                  {{'Teilnehmer' | customtext:'gm_col_person' | async}}
                </td>
                <td mat-sort-header="bookletName" *ngIf="displayOptions.bookletColumn === 'show'">
                  {{'Testheft' | customtext:'gm_col_booklet' | async}}
                </td>
                <td mat-sort-header="_currentBlock" *ngIf="displayOptions.blockColumn === 'show'">
                  {{'Block' | customtext:'gm_col_testlet' | async}}
                </td>
                <td mat-sort-header="timestamp">
                  {{'Aktivität' | customtext:'gm_col_activity' | async}}
                </td>
                <td mat-sort-header="_currentUnit" *ngIf="displayOptions.unitColumn === 'show'">
                  {{'Aufgabe' | customtext:'gm_col_unit' | async}}
                </td>
              </tr>
            </thead>

            <ng-container *ngFor="let session of tsm.sessions$ | async; trackBy: trackSession">
              <tc-test-session
                [testSession]="session"
                [displayOptions]="displayOptions"
                [marked]="markedElement"
                (markedElement$)="markElement($event)"
                [selected]="selectedElement"
                (selectedElement$)="selectElement($event)"
                [checked]="tsm.isChecked(session)"
                (checked$)="toggleChecked($event, session)"
                [ngStyle]="{background: getSessionColor(session)}"
              >
              </tc-test-session>
            </ng-container>
          </table>
        </div>
      </div>
    </mat-sidenav-content>
  </mat-sidenav-container>

  <button
    class="drawer-button-close"
    mat-icon-button
    (click)="sidenav.toggle()"
    matTooltip=""
    matTooltipPosition="right"
  >
    <mat-icon>chevron_right</mat-icon>
  </button>

  <button
    *ngIf="sidenav.opened"
    class="drawer-button-open"
    mat-icon-button
    (click)="sidenav.toggle()"
    matTooltip="{{'Test-Steuerung verbergen' | customtext:'gm_hide_controls_tooltip' | async}}"
    matTooltipPosition="above"
  >
    <mat-icon>chevron_left</mat-icon>
  </button>

</div>

<div id="shield" *ngIf="isClosing"></div>

./group-monitor.component.css

.page-body {
  overflow-x: hidden;
}

.test-session-table {
  border-collapse: collapse;
  display: table;
  width: 100%;
}

.test-session-table thead tr td {
  position: sticky;
  top: 0;
  z-index: 2;
  background: rgba(255, 255, 255, 0.8);
}

.test-session-table td {
  padding-top: 15px;
  margin-right: 1em;
}

.test-session-table td[mat-sort-header="_checked"] {
  padding-left: 5px;
}

.adminbackground {
  box-shadow: 5px 10px 20px black;
  background-color: white;
  margin: 0 0 0 15px;
  padding: 0 25px 25px 25px;
  height: 100%;
  overflow: auto;
  scroll-behavior: smooth;
}

.page-header {
  display: flex;
  flex-wrap: wrap;
}

.page-header > p:last-child {
  margin-right: 15px;
}

.page-header > div {
  text-align: right;
  vertical-align: middle;
}

.page-header .fill-remaining-space {
  flex: 1 1 auto;
}

.connection-status {
  text-transform: uppercase;
  color: white;
  font-size: 80%;
  justify-content: space-between;
  width: 120px
}

.connection-status mat-icon {
  font-size: 100%;
  padding: 0;
  top: 2px;
}

.connection-status.error {
  background: #821123
}

.connection-status.ws-offline {
  background: orange;
}

.connection-status.ws-online {
  animation-name: pulse;
  animation-duration: 2s;
  animation-iteration-count: infinite;
}

.connection-status.polling-fetch,
.connection-status.polling-sleep {
  position: relative;
  background-image: linear-gradient(90deg, rgba(0,146,0,1) 0%, rgba(0,199,0,1) 100%, rgba(0,146,0,1) 200%);
  background-repeat: repeat-x;
  background-position: 0;
  animation-name: move-gradient;
  animation-duration: 5s;
  animation-iteration-count: infinite;
}

@keyframes move-gradient {
  0% {
    background-position: 0;
  }
  100% {
    background-position: 120px;
  }
}

@keyframes pulse {
  0% {
    background: rgba(0,199,0,1);
  }
  50% {
    background: rgba(0,146,0,1);
  }
  100% {
    background: rgba(0,199,0,1);
  }
}

.toolbar {
  padding: 15px;
}

.toolbar h2 {
  margin-top: 0;
  font-size: 1.5em;
}

.toolbar .mat-radio-button ~ .mat-radio-button {
  margin-left: 16px;
}

.toolbar-section {
  margin-bottom: 1em
}

.toolbar button {
  text-transform: uppercase;
}

.toolbar .control ~ .control {
  margin-left: 15px;
}

.min-height-section {
  min-height: 3em;
}

.toolbar-section-bottom {
  position: absolute;
  bottom: 0;
}

mat-sidenav {
  width: 350px;
}

.mat-drawer-content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 25px;
  overflow: initial;
  height: initial;
}

.mat-drawer-container {
  position: initial;
  background: none;
}

.mat-drawer {
  margin: 15px 0 0 0;
}

.corner-menu {
  position: fixed;
  right: 10px;
  top: 72px;
  z-index: 10000;
  border-bottom-left-radius: 10px;
}

.scroll-hint {
  position: fixed;
  right: 15px;
  bottom: 15px;
  background: #b2ff59;
  box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
  border-radius: 20Px;
  height: 40px;
  width: 40px;
  line-height: 20px;
  text-align: center;
  z-index: 9000;
  color: #003333;
}

.hide-scroll-hint .scroll-hint {
  animation: fade-and-shrink 0.3s reverse forwards;
}

@keyframes fade-and-shrink {
  0% {
    opacity: 0;
    transform: scale(0)
  }

  100% {
    opacity: 1;
    transform: scale(1)
  }
}

.drawer-button-open {
  top: 50%;
  left: 320px;
  position: fixed;
  z-index: 10000;
}

.drawer-button-close {
  top: 50%;
  left: -12px;
  color: white;
  position: fixed;
  z-index: 10000;
}

.emph {
  color: #b2ff59;
  font-style: italic;
  text-transform: uppercase;
}

[disabled] .emph {
  color: #821123
}

#message-panel alert:first-of-type {
  background: #b2ff59;
  animation: fade 7s reverse forwards;
}


@keyframes fade {
  0% {
    opacity: 0.1;
  }

  100% {
    opacity: 1;
  }
}

#shield {
  position: fixed;
  background: white;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  animation: fade 6s forwards;
  z-index: 100000;
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""