Unsere GitLab-Installation wurde aktualisiert (Informationen zu den Neuerungen).

Add MemberGroup interface to the session configuration

parent f5454a3d
stages:
- build
- test
- deploy
build:
......@@ -15,6 +16,17 @@ build:
paths:
- dist
test:
stage: test
only:
- master
- CI
tags:
- nodejs
script:
- npm install
- npm test
deploy:
stage: deploy
only:
......
......@@ -8,5 +8,7 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="cookieconsent" level="application" />
<orderEntry type="library" name="all" level="application" />
</component>
</module>
\ No newline at end of file
This diff is collapsed.
......@@ -6,90 +6,90 @@
"type": "git",
"url": "git@git.thm.de:arsnova/arsnova.click-v2.git"
},
"browserslist": [
"last 2 versions",
"not ie <= 10",
"not ie_mob <= 10"
],
"description": "Version 2 of arsnova.click (Frontend WebApp)",
"scripts": {
"ng": "ng",
"start": "ng serve --host 0.0.0.0 --port 4200 --disable-host-check --aot",
"start:DEV": "ng serve --host 0.0.0.0 --port 4200 --disable-host-check --aot",
"build:DEV": "ng serve --host 0.0.0.0 --port 4200 --disable-host-check --prod --aot --build-optimizer",
"build:PROD": "ng build --output-path=dist/ --no-sourcemap --prod --aot --build-optimizer",
"build:PROD-STATS": "ng build --output-path=dist/ --no-sourcemap --prod --aot --build-optimizer --stats-json",
"build:PROD": "ng build --prod",
"build:PROD-STATS": "ng build --prod --stats-json",
"bundle-report": "webpack-bundle-analyzer dist/stats.json",
"test": "ng test",
"test": "ng test --browsers=ChromeHeadless --watch=false",
"lint": "ng lint",
"e2e": "ng e2e",
"pree2e": "webdriver-manager update --standalone false --gecko false",
"e2e": "ng e2e --no-webdriver-update",
"precompress": "node purifycss.js",
"compress": "gzip dist/** -r",
"prod-test": "npm run prod && npm run compress && http-server dist/ -p 4711 --gzip",
"prod": "npm run build:PROD",
"postprod": "npm run cleanup-sass && npm run imagemin && npm run cache",
"cleanup-sass": "rimraf dist/assets/fonts/font-awesome-4.7.0/scss",
"imagemin": "find dist/assets/icons/ -type d -exec imagemin {}/* --out-dir={} --plugin=pngquant \\;",
"cache": "sw-precache --config=sw-precache.config.js",
"postcache": "uglifyjs dist/service-worker.js --screw-ie8 --compress --mangle --output dist/service-worker.js"
"http-startup": "http-server dist/ -p 4711 --gzip",
"prod-test": "npm run build:PROD && npm run compress && npm run http-startup"
},
"private": true,
"dependencies": {
"@angular/animations": "^5.1.3",
"@angular/common": "^5.1.3",
"@angular/compiler": "^5.1.3",
"@angular/core": "^5.1.3",
"@angular/forms": "^5.1.3",
"@angular/http": "^5.1.3",
"@angular/platform-browser": "^5.1.3",
"@angular/platform-browser-dynamic": "^5.1.3",
"@angular/router": "^5.1.3",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.8",
"@ngx-translate/core": "^8.0.0",
"@ngx-translate/http-loader": "^2.0.1",
"@angular/animations": "^6.0.0",
"@angular/common": "^6.0.0",
"@angular/compiler": "^6.0.0",
"@angular/core": "^6.0.0",
"@angular/forms": "^6.0.0",
"@angular/http": "^6.0.0",
"@angular/platform-browser": "^6.0.0",
"@angular/platform-browser-dynamic": "^6.0.0",
"@angular/router": "^6.0.0",
"@angular/service-worker": "^6.0.0",
"@ng-bootstrap/ng-bootstrap": "^2.0.0-alpha.0",
"@ng-bootstrap/schematics": "^2.0.0-alpha.1",
"@ngx-translate/core": "^10.0.1",
"@ngx-translate/http-loader": "~3.0.1",
"@techiediaries/ngx-qrcode": "0.0.5",
"bootstrap": "^4.0.0-beta.3",
"enhanced-resolve": "^3.4.1",
"angulartics2": "^6.0.0",
"bootstrap": "^4.0.0",
"classlist.js": "^1.1.20150312",
"highlight.js": "^9.12.0",
"intro.js": "^2.8.0-alpha.1",
"jquery": "^3.2.1",
"intro.js": "^2.9.3",
"marked": "git+https://github.com/trayhem/marked.git",
"messageformat": "^1.1.0",
"ngx-qrcode2": "0.0.5",
"ngx-translate-messageformat-compiler": "^2.1.0",
"popper.js": "^1.13.0",
"rxjs": "^5.5.6",
"zone.js": "^0.8.19"
"messageformat": "^2.0.2",
"ngx-qrcode2": "^0.1.0",
"ngx-translate-messageformat-compiler": "^4.0.0",
"npm": "^6.0.1",
"rxjs": "^6.0.0",
"zone.js": "^0.8.26"
},
"devDependencies": {
"@angular/cli": "^1.6.3",
"@angular/compiler-cli": "^5.1.3",
"@angular/language-service": "^5.1.3",
"@types/jasmine": "^2.8.3",
"@angular-devkit/build-angular": "^0.6.0",
"@angular/cli": "^6.0.0",
"@angular/compiler-cli": "^6.0.0",
"@angular/language-service": "^6.0.0",
"@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^8.5.5",
"@types/node": "^10.0.9",
"@types/webpack": "~3.8.5",
"arsnova-click-v2-types": "git+https://git.thm.de/arsnova/arsnova-click-v2-types.git",
"codelyzer": "^4.0.2",
"http-server": "^0.10.0",
"imagemin-pngquant": "^5.0.1",
"jasmine-core": "~2.8.0",
"codelyzer": "^4.3.0",
"http-server": "^0.11.1",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.1",
"karma-chrome-launcher": "~2.2.0",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.3.3",
"karma-jasmine": "^1.1.1",
"karma-coverage-istanbul-reporter": "~1.4.2",
"karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2",
"node-sass": "^4.7.2",
"protractor": "^5.2.2",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.2",
"sass-loader": "^6.0.3",
"sw-precache": "^5.2.0",
"ts-node": "~3.3.0",
"tslint": "~5.8.0",
"typescript": "2.4.2",
"uglify-js": "^3.3.4"
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.4",
"protractor": "^5.3.2",
"purify-css": "~1.2.5",
"ts-node": "~5.0.1",
"tslint": "~5.9.1",
"typescript": "~2.7.2"
},
"keywords": [
"arsnova",
"arsnova.click",
"Audience",
"Response",
"System"
"Audience Response System"
],
"author": "Christopher Fullarton"
}
......@@ -9,20 +9,25 @@ exports.config = {
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
'browserName': 'chrome',
chromeOptions: {
args: [ "--headless", "--disable-gpu", "--window-size=800x600", '--no-sandbox' ]
}
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
directConnect: false,
baseUrl: 'http://localhost:49153/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
beforeLaunch: function() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
},
onPrepare() {
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};
<nav *ngIf="footerElements.length"
id="navbar-footer-container"
class="nav navbar-footer flex-nowrap flex-sm-column justify-content-sm-start p-1"
role="navigation">
<input class="hidden" id="upload-session" type="file" (change)="fileChange($event)" accept="application/json" multiple="multiple"/>
<div *ngFor="let elem of footerElements"
class="footerElement py-sm-2 px-sm-1 justify-content-center justify-content-sm-start"
class="footerElement px-2 py-2 px-sm-1 py-sm-2 pointer d-flex align-items-center justify-content-center justify-content-sm-start"
[class.success]="elem.selectable && elem.isActive"
[class.error]="elem.selectable && !elem.isActive"
[id]="elem.id"
[routerLink]="getLinkTarget(elem)"
[title]="elem.textName | translate"
[attr.data-intro]="elem.showIntro ? (('region.footer.footer_bar.description.' + elem.id) | translate) : null"
(click)="toggleSetting(elem)">
<p class="footerElemIcon"><i [class]="elem.iconClass" aria-hidden="true"></i></p>
<p class="footerElemText text-left ml-2"><span [class]="elem.textClass" [innerHTML]="elem.textName | translate"></span></p>
<p class="footerElemIcon mb-0"><i [class]="elem.iconClass" aria-hidden="true"></i></p>
<p class="footerElemText text-truncate text-left mb-0 ml-2 d-md-block"><span [class]="elem.textClass" [innerHTML]="elem.textName | translate"></span></p>
</div>
</nav>
<span class="before d-sm-none d-none position-absolute justify-content-center align-items-center h-100"
(click)="moveLeft()">&#x21E6;</span>
<span class="after d-sm-none d-flex position-absolute justify-content-center align-items-center h-100"
*ngIf="hasRightScrollElement"
(click)="moveRight()">&#x21E8;</span>
\ No newline at end of file
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {FooterbarElement, FooterBarService} from '../../service/footer-bar.service';
import {Router} from '@angular/router';
import {Subscription} from 'rxjs/Subscription';
import {Subscription} from 'rxjs';
import {FileUploadService} from '../../service/file-upload.service';
import {CurrentQuizService} from '../../service/current-quiz.service';
import {TrackingService} from '../../service/tracking.service';
@Component({
selector: 'app-footer-bar',
......@@ -11,24 +12,54 @@ import {CurrentQuizService} from '../../service/current-quiz.service';
styleUrls: ['./footer-bar.component.scss']
})
export class FooterBarComponent implements OnInit, OnDestroy {
@Input()
set footerElements(value: Array<FooterbarElement>) {
this.hasRightScrollElement = value.length > 1;
this._footerElements = value;
}
get hasRightScrollElement(): boolean {
return this._hasRightScrollElement;
}
set hasRightScrollElement(value: boolean) {
this._hasRightScrollElement = value;
}
get footerElemIndex(): number {
return this._footerElemIndex;
}
set footerElemIndex(value: number) {
this._footerElemIndex = value;
}
public static TYPE = 'FooterBarComponent';
get _footerElements(): Array<FooterbarElement> {
return this.footerElements;
get footerElements(): Array<FooterbarElement> {
return this._footerElements;
}
@Input() footerElements: Array<FooterbarElement> = [];
private _footerElements: Array<FooterbarElement> = [];
private _routerSubscription: Subscription;
private _footerElemIndex = 1;
private _hasRightScrollElement = false;
constructor(
private router: Router,
private footerBarService: FooterBarService,
private currentQuizService: CurrentQuizService,
private fileUploadService: FileUploadService) {
private trackingService: TrackingService,
private fileUploadService: FileUploadService
) {
}
ngOnInit() {
this._routerSubscription = this.router.events.subscribe((val) => {
const navbarFooter = document.getElementById('navbar-footer-container');
this.footerElemIndex = 1;
if (navbarFooter) {
navbarFooter.scrollLeft = 0;
this.hideElement('left');
}
if (val.hasOwnProperty('url')) {
this.footerBarService.footerElemTheme.linkTarget = val['url'].indexOf('lobby') > -1 ? '/quiz/flow/theme' : '/themes';
}
......@@ -43,12 +74,20 @@ export class FooterBarComponent implements OnInit, OnDestroy {
return typeof elem.linkTarget === 'function' ? elem.linkTarget(elem) : elem.linkTarget;
}
toggleSetting(elem: FooterbarElement) {
toggleSetting(elem: FooterbarElement): void {
this.currentQuizService.toggleSetting(elem);
elem.onClickCallback(elem);
this.trackingService.trackClickEvent({
action: this.footerBarService.TYPE_REFERENCE,
label: `footer-${elem.id}`,
customDimensions: {
dimension1: elem.selectable,
dimension2: elem.isActive
}
});
}
public fileChange(event: any) {
public fileChange(event: any): void {
const fileList: FileList = event.target.files;
if (!fileList.length) {
return;
......@@ -60,4 +99,57 @@ export class FooterBarComponent implements OnInit, OnDestroy {
}
this.fileUploadService.uploadFile(formData);
}
private hideElement(elem: 'left'): void {
const element = this.getElement(elem);
element.classList.add('d-none');
element.classList.remove('d-flex');
}
private showElement(elem: 'left'): void {
const element = this.getElement(elem);
element.classList.remove('d-none');
element.classList.add('d-flex');
}
private getElement(elem: 'left' | 'right'): Element {
let className = '';
switch (elem) {
case 'left':
className = 'before';
break;
case 'right':
className = 'after';
break;
}
return document.getElementsByClassName('footer-bar-wrapper').item(0).getElementsByClassName(className).item(0);
}
public moveLeft(): void {
const navbarFooter = document.getElementById('navbar-footer-container');
const right = navbarFooter.children.item(--this.footerElemIndex).getBoundingClientRect().right;
const firstChild = navbarFooter.children.item(1);
navbarFooter.scrollLeft += right;
this.hasRightScrollElement = true;
if (firstChild.getBoundingClientRect().right >= 0) {
this.hideElement('left');
}
}
public moveRight(): void {
const navbarFooter = document.getElementById('navbar-footer-container');
const right = navbarFooter.children.item(++this.footerElemIndex).getBoundingClientRect().right;
const lastChild = navbarFooter.children.item(navbarFooter.children.length - 1);
navbarFooter.scrollLeft += right;
this.showElement('left');
if (navbarFooter.scrollLeft >= lastChild.getBoundingClientRect().left) {
this.hasRightScrollElement = false;
}
}
}
......@@ -41,19 +41,6 @@
</nav>
<ng-template #connectionQualityModal let-c="close" let-d="dismiss">
<style>
li {
list-style: none;
}
.fa-check-square {
color: green;
}
.fa-square {
color: red;
}
</style>
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel" [innerHTML]="'region.header.connection_status.title' | translate"></h5>
<button type="button" class="close pointer" data-dismiss="modal" aria-label="Close" (click)="d()">
......@@ -61,47 +48,67 @@
</button>
</div>
<div class="modal-body">
<ul class="p-0">
<ul class="p-0 list-unstyled">
<li>
<i class="far"
<i class="fas"
[class.fa-check-square]="connectionService.serverAvailable"
[class.fa-square]="!connectionService.serverAvailable"></i>
[class.text-success]="connectionService.serverAvailable"
[class.fa-times]="!connectionService.serverAvailable"
[class.text-danger]="!connectionService.serverAvailable"
data-fa-transform="shrink-5"
data-fa-mask="fas fa-square"></i>
<p class="mb-0 d-inline">
<span>Server: </span>
<span>{{'region.header.connection_status.server_status.' + (connectionService.serverAvailable ? 'available' : 'not_available') | translate}}</span>
</p>
</li>
<li>
<i class="far"
<i class="fas"
[class.fa-check-square]="connectionService.websocketAvailable"
[class.fa-square]="!connectionService.websocketAvailable"></i>
[class.text-success]="connectionService.websocketAvailable"
[class.fa-times]="!connectionService.websocketAvailable"
[class.text-danger]="!connectionService.websocketAvailable"
data-fa-transform="shrink-5"
data-fa-mask="fas fa-square"></i>
<p class="mb-0 d-inline">
<span>Websocket: </span>
<span>{{'region.header.connection_status.websocket_status.' + (connectionService.websocketAvailable ? 'connected' : 'not_connected') | translate}}</span>
</p>
</li>
<li>
<i class="far"
<i class="fas"
[class.fa-check-square]="localStorageAvailable"
[class.fa-square]="!localStorageAvailable"></i>
[class.text-success]="localStorageAvailable"
[class.fa-times]="!localStorageAvailable"
[class.text-danger]="!localStorageAvailable"
data-fa-transform="shrink-5"
data-fa-mask="fas fa-square"></i>
<p class="mb-0 d-inline">
<span>LocalStorage: </span>
<span>{{'region.header.connection_status.localStorage_status.' + (localStorageAvailable ? 'writable' : 'non_writable') | translate}}</span>
</p>
</li>
<li>
<i class="far"
<i class="fas"
[class.fa-check-square]="sessionStorageAvailable"
[class.fa-square]="!sessionStorageAvailable"></i>
[class.text-success]="sessionStorageAvailable"
[class.fa-times]="!sessionStorageAvailable"
[class.text-danger]="!sessionStorageAvailable"
data-fa-transform="shrink-5"
data-fa-mask="fas fa-square"></i>
<p class="mb-0 d-inline">
<span>SessionStorage: </span>
<span>{{'region.header.connection_status.sessionStorage_status.' + (sessionStorageAvailable ? 'writable' : 'non_writable') | translate}}</span>
</p>
</li>
<li>
<i class="far"
<i class="fas"
[class.fa-check-square]="connectionService.rtt < 300 && connectionService.serverAvailable"
[class.fa-square]="connectionService.rtt > 300 || !connectionService.serverAvailable"></i>
[class.text-success]="connectionService.rtt < 300 && connectionService.serverAvailable"
[class.fa-times]="connectionService.rtt > 300 || !connectionService.serverAvailable"
[class.text-danger]="connectionService.rtt > 300 || !connectionService.serverAvailable"
data-fa-transform="shrink-5"
data-fa-mask="fas fa-square"></i>
<p class="mb-0 d-inline">
<span>Round-Trip-Time: </span>
<span>{{connectionService.serverAvailable ? (connectionService.rtt + 'ms') : ('region.header.connection_status.server_status.not_available' | translate)}}</span>
......
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {HeaderComponent} from './header.component';
import {TranslateCompiler, TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {TranslateMessageFormatCompiler} from 'ngx-translate-messageformat-compiler';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {createTranslateLoader} from '../../../lib/translation.factory';
import {RouterTestingModule} from '@angular/router/testing';
import {ArsnovaClickAngulartics2Piwik} from '../../shared/tracking/ArsnovaClickAngulartics2Piwik';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {Angulartics2Module} from 'angulartics2';
import {WebsocketService} from '../../service/websocket.service';
import {TrackingService} from '../../service/tracking.service';
import {SharedService} from '../../service/shared.service';
import {ConnectionService} from '../../service/connection.service';
import {HeaderLabelService} from '../../service/header-label.service';
import {WebsocketMockService} from '../../service/websocket.mock.service';
import {ConnectionMockService} from '../../service/connection.mock.service';
import {TrackingMockService} from '../../service/tracking.mock.service';
describe('HeaderComponent', () => {
let component: HeaderComponent;
......@@ -8,18 +24,46 @@ describe('HeaderComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HeaderComponent]
})
.compileComponents();
imports: [
RouterTestingModule,
HttpClientModule,
NgbModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
},
compiler: {
provide: TranslateCompiler,
useClass: TranslateMessageFormatCompiler
}
}),
],
providers: [
HeaderLabelService,
{provide: ConnectionService, useClass: ConnectionMockService},
{provide: TrackingService, useClass: TrackingMockService},
SharedService,
{provide: WebsocketService, useClass: WebsocketMockService},
],
declarations: [
HeaderComponent
]
}).compileComponents();
}));
beforeEach(() => {
beforeEach(async(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
}));
it('should be created', () => {
it('should be created', async(() => {
expect(component).toBeTruthy();
});
}));
it('should contain a TYPE definition', async(() => {
expect(HeaderComponent.TYPE).toEqual('HeaderComponent');
}));
});
......@@ -3,6 +3,7 @@ import {Router} from '@angular/router';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {ConnectionService} from '../../service/connection.service';
import {HeaderLabelService} from '../../service/header-label.service';
import {TrackingService} from '../../service/tracking.service';
function isLocalStorageSupported(): boolean {
try {
......@@ -42,6 +43,8 @@ function isSessionStorageSupported(): boolean {
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
public static TYPE = 'HeaderComponent';
get localStorageAvailable(): boolean {
return this._localStorageAvailable;
}
......@@ -80,7 +83,9 @@ export class HeaderComponent implements OnInit {
private router: Router,
private modalService: NgbModal,
public headerLabelService: HeaderLabelService,
public connectionService: ConnectionService) {
public connectionService: ConnectionService,
private trackingService: TrackingService
) {
}
ngOnInit() {
......@@ -90,6 +95,20 @@ export class HeaderComponent implements OnInit {
}
openConnectionQualityModal(content: string): void {
this.trackingService.trackClickEvent({
action: 'ConnectionQualityModal',
label: 'open-dialog',
customDimensions: {
dimension1: this.connectionService.lowSpeed,
dimension2: this.connectionService.mediumSpeed,
dimension3: this.connectionService.rtt,
dimension4: this.connectionService.serverAvailable,
dimension5: this.connectionService.websocketAvailable,
dimension6: this.localStorageAvailable,
dimension7: this.sessionStorageAvailable
}
});
this.modalService.open(content);
this.connectionService.calculateRTT();
}
......
.preview-frame {
border: 20px solid black;
border-radius: 20px;
border-bottom-width: 50px;
@import "../../../styles_vendor";
height: calc(90vh - 20px);
width: 360px;
.preview-frame {
padding: 0 10px 10px 10px;