File

src/app/group-monitor/test-session-manager/test-session-manager.service.ts

Index

Properties
Methods
Accessors

Constructor

constructor(bs: BackendService, bookletService: BookletService)
Parameters :
Name Type Optional
bs BackendService No
bookletService BookletService No

Methods

Private Static applyFilters
applyFilters(session: TestSession, filters: TestSessionFilter[])
Parameters :
Name Type Optional
session TestSession No
filters TestSessionFilter[] No
Returns : boolean
checkAll
checkAll()
Returns : void
checkNone
checkNone()
Returns : void
checkSession
checkSession(session: TestSession)
Parameters :
Name Type Optional
session TestSession No
Returns : void
checkSessionsBySelection
checkSessionsBySelection(selected: Selected)
Parameters :
Name Type Optional
selected Selected No
Returns : void
commandFinishEverything
commandFinishEverything()
Returns : Observable<CommandResponse>
connect
connect(groupName: string)
Parameters :
Name Type Optional
groupName string No
Returns : void
disconnect
disconnect()
Returns : void
Private Static filterSessions
filterSessions(sessions: TestSession[], filters: TestSessionFilter[])
Parameters :
Name Type Optional
sessions TestSession[] No
filters TestSessionFilter[] No
Returns : TestSession[]
Private Static getEmptyStats
getEmptyStats()
Private Static getSessionSetStats
getSessionSetStats(sessionSet: TestSession[], all: number)
Parameters :
Name Type Optional Default value
sessionSet TestSession[] No
all number No sessionSet.length
Private Static groupForGoto
groupForGoto(sessionsSet: TestSession[], selection: Selected)
Parameters :
Name Type Optional
sessionsSet TestSession[] No
selection Selected No
Returns : GotoCommandData
invertChecked
invertChecked()
Returns : void
isChecked
isChecked(session: TestSession)
Parameters :
Name Type Optional
session TestSession No
Returns : boolean
Private onCheckedChanged
onCheckedChanged()
Returns : void
Private replaceCheckedSessions
replaceCheckedSessions(sessionsToCheck: TestSession[])
Parameters :
Name Type Optional
sessionsToCheck TestSession[] No
Returns : void
sortSessions
sortSessions(sort: Sort, sessions: TestSession[])
Parameters :
Name Type Optional
sort Sort No
sessions TestSession[] No
Returns : TestSession[]
switchFilter
switchFilter(indexInFilterOptions: number)
Parameters :
Name Type Optional
indexInFilterOptions number No
Returns : void
Private synchronizeChecked
synchronizeChecked(sessions: TestSession[])
Parameters :
Name Type Optional
sessions TestSession[] No
Returns : void
testCommandGoto
testCommandGoto(selection: Selected)
Parameters :
Name Type Optional
selection Selected No
Returns : Observable<undefined>
testCommandPause
testCommandPause()
Returns : void
testCommandResume
testCommandResume()
Returns : void
testCommandUnlock
testCommandUnlock()
Returns : void
uncheckSession
uncheckSession(session: TestSession)
Parameters :
Name Type Optional
session TestSession No
Returns : void

Properties

Private _checked
Type : literal type
Default value : {}
Private _checkedStats$
Type : BehaviorSubject<TestSessionSetStats>
Private _commandResponses$
Type : Subject<CommandResponse>
Private _sessions$
Type : BehaviorSubject<TestSession[]>
Private _sessionsStats$
Type : BehaviorSubject<TestSessionSetStats>
checkingOptions
Type : CheckingOptions
filterOptions
Type : literal type[]
Default value : [ { label: 'gm_filter_locked', selected: false, filter: { type: 'testState', value: 'status', subValue: 'locked' } }, { label: 'gm_filter_pending', selected: false, filter: { type: 'testState', value: 'status', subValue: 'pending' } } ]
filters$
Type : Subject<TestSessionFilter[]>
Private groupName
Type : string
Private monitor$
Type : Observable<TestSession[]>
sortBy$
Type : Subject<Sort>

Accessors

sessions$
getsessions$()
sessions
getsessions()
checked
getchecked()
sessionsStats$
getsessionsStats$()
checkedStats$
getcheckedStats$()
commandResponses$
getcommandResponses$()
import { Injectable } from '@angular/core';
import {
  BehaviorSubject, combineLatest, Observable, Subject, zip
} from 'rxjs';
import { Sort } from '@angular/material/sort';
import {
  delay, flatMap, map, switchMap, tap
} from 'rxjs/operators';
import { BackendService } from '../backend.service';
import { BookletService } from '../booklet/booklet.service';
import { TestSessionUtil } from '../test-session/test-session.util';
import {
  isBooklet,
  Selected, CheckingOptions,
  TestSession,
  TestSessionFilter, TestSessionSetStats,
  TestSessionsSuperStates, CommandResponse, GotoCommandData
} from '../group-monitor.interfaces';
import { BookletUtil } from '../booklet/booklet.util';

@Injectable()
export class TestSessionManager {
  sortBy$: Subject<Sort>;
  filters$: Subject<TestSessionFilter[]>;
  checkingOptions: CheckingOptions;

  private groupName: string;

  get sessions$(): Observable<TestSession[]> {
    return this._sessions$.asObservable();
  }

  get sessions(): TestSession[] {
    return this._sessions$.getValue();
  }

  get checked(): TestSession[] { // this is intentionally not an observable
    return Object.values(this._checked);
  }

  get sessionsStats$(): Observable<TestSessionSetStats> {
    return this._sessionsStats$.asObservable();
  }

  get checkedStats$(): Observable<TestSessionSetStats> {
    return this._checkedStats$.asObservable();
  }

  get commandResponses$(): Observable<CommandResponse> {
    return this._commandResponses$.asObservable();
  }

  private monitor$: Observable<TestSession[]>;
  private _sessions$: BehaviorSubject<TestSession[]>;
  private _checked: { [sessionTestSessionId: number]: TestSession } = {};
  private _checkedStats$: BehaviorSubject<TestSessionSetStats>;
  private _sessionsStats$: BehaviorSubject<TestSessionSetStats>;
  private _commandResponses$: Subject<CommandResponse>;

  // attention: this works the other way round than Array.filter:
  // it defines which sessions are to filter out, not which ones are to keep
  filterOptions: { label: string, filter: TestSessionFilter, selected: boolean }[] = [
    {
      label: 'gm_filter_locked',
      selected: false,
      filter: {
        type: 'testState',
        value: 'status',
        subValue: 'locked'
      }
    },
    {
      label: 'gm_filter_pending',
      selected: false,
      filter: {
        type: 'testState',
        value: 'status',
        subValue: 'pending'
      }
    }
  ];

  constructor(
    private bs: BackendService,
    private bookletService: BookletService
  ) {}

  connect(groupName: string): void {
    this.groupName = groupName;
    this.sortBy$ = new BehaviorSubject<Sort>({ direction: 'asc', active: 'personLabel' });
    this.filters$ = new BehaviorSubject<TestSessionFilter[]>([]);
    this.checkingOptions = {
      enableAutoCheckAll: true,
      autoCheckAll: true
    };

    this._checkedStats$ = new BehaviorSubject<TestSessionSetStats>(TestSessionManager.getEmptyStats());
    this._sessionsStats$ = new BehaviorSubject<TestSessionSetStats>(TestSessionManager.getEmptyStats());
    this._commandResponses$ = new Subject<CommandResponse>();

    this.monitor$ = this.bs.observeSessionsMonitor()
      .pipe(
        switchMap(sessions => zip(...sessions
          .map(session => this.bookletService.getBooklet(session.bookletName)
            .pipe(
              map(booklet => TestSessionUtil.analyzeTestSession(session, booklet))
            ))))
      );

    this._sessions$ = new BehaviorSubject<TestSession[]>([]);
    combineLatest<[Sort, TestSessionFilter[], TestSession[]]>([this.sortBy$, this.filters$, this.monitor$])
      .pipe(
        // eslint-disable-next-line max-len
        map(([sortBy, filters, sessions]) => this.sortSessions(sortBy, TestSessionManager.filterSessions(sessions, filters))),
        tap(sessions => this.synchronizeChecked(sessions))
      )
      .subscribe(this._sessions$);
  }

  disconnect(): void {
    this.groupName = undefined;
    this.bs.cutConnection();
  }

  switchFilter(indexInFilterOptions: number): void {
    this.filterOptions[indexInFilterOptions].selected =
      !this.filterOptions[indexInFilterOptions].selected;
    this.filters$.next(
      this.filterOptions
        .filter(filterOption => filterOption.selected)
        .map(filterOption => filterOption.filter)
    );
  }

  private static filterSessions(sessions: TestSession[], filters: TestSessionFilter[]): TestSession[] {
    return sessions
      .filter(session => session.data.testId && session.data.testId > -1) // testsession without testId is deprecated
      .filter(session => TestSessionManager.applyFilters(session, filters));
  }

  private static applyFilters(session: TestSession, filters: TestSessionFilter[]): boolean {
    const applyNot = (isMatching, not: boolean): boolean => (not ? !isMatching : isMatching);
    return filters.reduce((keep: boolean, nextFilter: TestSessionFilter) => {
      switch (nextFilter.type) {
        case 'groupName': {
          return keep && applyNot(session.data.groupName !== nextFilter.value, nextFilter.not);
        }
        case 'bookletName': {
          return keep && applyNot(session.data.bookletName !== nextFilter.value, nextFilter.not);
        }
        case 'testState': {
          const keyExists = (typeof session.data.testState[nextFilter.value] !== 'undefined');
          const valueMatches = keyExists && (session.data.testState[nextFilter.value] === nextFilter.subValue);
          const keepIn = (typeof nextFilter.subValue !== 'undefined') ? !valueMatches : !keyExists;
          return keep && applyNot(keepIn, nextFilter.not);
        }
        case 'state': {
          return keep && applyNot(session.state !== nextFilter.value, nextFilter.not);
        }
        case 'bookletSpecies': {
          return keep && applyNot(session.booklet.species !== nextFilter.value, nextFilter.not);
        }
        case 'mode': {
          return keep && applyNot(session.data.mode !== nextFilter.value, nextFilter.not);
        }
        default:
          return false;
      }
    }, true);
  }

  private static getEmptyStats(): TestSessionSetStats {
    return {
      ...{
        all: false,
        number: 0,
        differentBookletSpecies: 0,
        differentBooklets: 0,
        paused: 0,
        locked: 0
      }
    };
  }

  private synchronizeChecked(sessions: TestSession[]): void {
    const sessionsStats = TestSessionManager.getSessionSetStats(sessions);

    this.checkingOptions.enableAutoCheckAll = (sessionsStats.differentBookletSpecies < 2);

    if (!this.checkingOptions.enableAutoCheckAll) {
      this.checkingOptions.autoCheckAll = false;
    }

    const newCheckedSessions: { [sessionFullId: number]: TestSession } = {};
    sessions
      .forEach(session => {
        if (this.checkingOptions.autoCheckAll || (typeof this._checked[session.data.testId] !== 'undefined')) {
          newCheckedSessions[session.data.testId] = session;
        }
      });
    this._checked = newCheckedSessions;

    this._checkedStats$.next(TestSessionManager.getSessionSetStats(Object.values(this._checked), sessions.length));
    this._sessionsStats$.next(sessionsStats);
  }

  sortSessions(sort: Sort, sessions: TestSession[]): TestSession[] {
    return sessions
      .sort((session1, session2) => {
        const sortDirectionFactor = (sort.direction === 'asc' ? 1 : -1);
        if (sort.active === 'timestamp') {
          return (session1.data.timestamp - session2.data.timestamp) * sortDirectionFactor;
        }
        if (sort.active === '_checked') {
          const session1isChecked = this.isChecked(session1);
          const session2isChecked = this.isChecked(session2);
          if (!session1isChecked && session2isChecked) {
            return 1 * sortDirectionFactor;
          }
          if (session1isChecked && !session2isChecked) {
            return -1 * sortDirectionFactor;
          }
          return 0;
        }
        if (sort.active === '_superState') {
          return (TestSessionsSuperStates.indexOf(session1.state) -
            TestSessionsSuperStates.indexOf(session2.state)) * sortDirectionFactor;
        }
        if (sort.active === '_currentBlock') {
          const s1curBlock = session1.current?.ancestor?.blockId || 'zzzzzzzzzz';
          const s2curBlock = session2.current?.ancestor?.blockId || 'zzzzzzzzzz';
          return s1curBlock.localeCompare(s2curBlock) * sortDirectionFactor;
        }
        if (sort.active === '_currentUnit') {
          const s1currentUnit = session1.current ? session1.current.unit.label : 'zzzzzzzzzz';
          const s2currentUnit = session2.current ? session2.current.unit.label : 'zzzzzzzzzz';
          return s1currentUnit.localeCompare(s2currentUnit) * sortDirectionFactor;
        }
        const stringA = (session1.data[sort.active] || 'zzzzzzzzzz');
        const stringB = (session2.data[sort.active] || 'zzzzzzzzzz');
        return stringA.localeCompare(stringB) * sortDirectionFactor;
      });
  }

  testCommandPause(): void {
    const testIds = this.checked
      .filter(session => !TestSessionUtil.isPaused(session))
      .map(session => session.data.testId);
    if (!testIds.length) {
      this._commandResponses$.next({ commandType: 'pause', testIds });
      return;
    }
    this.bs.command('pause', [], testIds).subscribe(
      response => this._commandResponses$.next(response)
    );
  }

  testCommandResume(): void {
    const testIds = this.checked
      .map(session => session.data.testId);
    if (!testIds.length) {
      this._commandResponses$.next({ commandType: 'resume', testIds });
      return;
    }
    this.bs.command('resume', [], testIds).subscribe(
      response => this._commandResponses$.next(response)
    );
  }

  testCommandGoto(selection: Selected): Observable<true> {
    const gfd = TestSessionManager.groupForGoto(this.checked, selection);
    const allTestIds = this.checked.map(s => s.data.testId);
    return zip(
      ...Object.keys(gfd).map(key => this.bs.command('goto', ['id', gfd[key].firstUnitId], gfd[key].testIds))
    ).pipe(
      tap(() => {
        this._commandResponses$.next({
          commandType: 'goto',
          testIds: allTestIds
        });
      }),
      map(() => true)
    );
  }

  private static groupForGoto(sessionsSet: TestSession[], selection: Selected): GotoCommandData {
    const groupedByBooklet: GotoCommandData = {};
    sessionsSet.forEach(session => {
      if (!groupedByBooklet[session.data.bookletName] && isBooklet(session.booklet)) {
        const firstUnit = BookletUtil.getFirstUnitOfBlock(selection.element.blockId, session.booklet);
        if (firstUnit) {
          groupedByBooklet[session.data.bookletName] = {
            testIds: [],
            firstUnitId: firstUnit.id
          };
        }
      }
      if (groupedByBooklet[session.data.bookletName]) {
        groupedByBooklet[session.data.bookletName].testIds.push(session.data.testId);
      }
    });
    return groupedByBooklet;
  }

  testCommandUnlock(): void {
    const testIds = this.checked
      .filter(TestSessionUtil.isLocked)
      .map(session => session.data.testId);
    if (!testIds.length) {
      this._commandResponses$.next({ commandType: 'unlock', testIds });
      return;
    }
    this.bs.unlock(this.groupName, testIds).subscribe(
      response => this._commandResponses$.next(response)
    );
  }

  // todo unit test
  commandFinishEverything(): Observable<CommandResponse> {
    const getUnlockedConnectedTestIds = () => Object.values(this._sessions$.getValue())
      .filter(session => !TestSessionUtil.hasState(session.data.testState, 'status', 'locked') &&
        !TestSessionUtil.hasState(session.data.testState, 'CONTROLLER', 'TERMINATED') &&
        (TestSessionUtil.hasState(session.data.testState, 'CONNECTION', 'POLLING') ||
          TestSessionUtil.hasState(session.data.testState, 'CONNECTION', 'WEBSOCKET')))
      .map(session => session.data.testId);
    const getUnlockedTestIds = () => Object.values(this._sessions$.getValue())
      .filter(session => session.data.testId > 0)
      .filter(session => !TestSessionUtil.hasState(session.data.testState, 'status', 'locked'))
      .map(session => session.data.testId);

    this.filters$.next([]);

    return this.bs.command('terminate', [], getUnlockedConnectedTestIds())
      .pipe(
        delay(1900),
        flatMap(() => this.bs.lock(this.groupName, getUnlockedTestIds()))
      );
  }

  isChecked(session: TestSession): boolean {
    return (typeof this._checked[session.data.testId] !== 'undefined');
  }

  checkSessionsBySelection(selected: Selected): void {
    if (this.checkingOptions.autoCheckAll) {
      return;
    }
    let toCheck: TestSession[] = [];
    if (selected.element) {
      if (!selected.spreading) {
        toCheck = [...this.checked, selected.originSession];
      } else {
        toCheck = this._sessions$.getValue()
          .filter(session => (session.booklet.species === selected.originSession.booklet.species))
          .filter(session => (selected.inversion ? !this.isChecked(session) : true));
      }
    }

    this.replaceCheckedSessions(toCheck);
  }

  invertChecked(): void {
    if (this.checkingOptions.autoCheckAll) {
      return;
    }
    const unChecked = this._sessions$.getValue()
      .filter(session => session.data.testId && session.data.testId > -1)
      .filter(session => !this.isChecked(session));
    this.replaceCheckedSessions(unChecked);
  }

  checkSession(session: TestSession): void {
    if (this.checkingOptions.autoCheckAll) {
      return;
    }
    this._checked[session.data.testId] = session;
    this.onCheckedChanged();
  }

  uncheckSession(session: TestSession): void {
    if (this.checkingOptions.autoCheckAll) {
      return;
    }
    if (this.isChecked(session)) {
      delete this._checked[session.data.testId];
    }
    this.onCheckedChanged();
  }

  checkAll(): void {
    if (this.checkingOptions.autoCheckAll) {
      return;
    }
    this.replaceCheckedSessions(this._sessions$.getValue());
  }

  checkNone(): void {
    if (this.checkingOptions.autoCheckAll) {
      return;
    }
    this.replaceCheckedSessions([]);
  }

  private replaceCheckedSessions(sessionsToCheck: TestSession[]): void {
    const newCheckedSessions = {};
    sessionsToCheck
      .forEach(session => { newCheckedSessions[session.data.testId] = session; });
    this._checked = newCheckedSessions;
    this.onCheckedChanged();
  }

  private onCheckedChanged(): void {
    this._checkedStats$.next(TestSessionManager.getSessionSetStats(this.checked, this.sessions.length));
  }

  private static getSessionSetStats(sessionSet: TestSession[], all: number = sessionSet.length): TestSessionSetStats {
    const booklets = new Set();
    const bookletSpecies = new Set();
    let paused = 0;
    let locked = 0;

    sessionSet
      .forEach(session => {
        booklets.add(session.data.bookletName);
        bookletSpecies.add(session.booklet.species);
        if (TestSessionUtil.isPaused(session)) paused += 1;
        if (TestSessionUtil.isLocked(session)) locked += 1;
      });

    return {
      number: sessionSet.length,
      differentBooklets: booklets.size,
      differentBookletSpecies: bookletSpecies.size,
      all: (all === sessionSet.length),
      paused,
      locked
    };
  }
}

result-matching ""

    No results matching ""