feature flag support
This commit is contained in:
parent
dd9932d2db
commit
3bb63d1d5a
27 changed files with 264 additions and 18 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
export const API_URL = 'http://127.0.0.1:3000';
|
export const API_URL = 'http://127.0.0.1:3000';
|
||||||
//export const WEBSOCK_URL = 'http://127.0.0.1:3000';
|
export const WEBSOCK_URL = 'http://127.0.0.1:3000';
|
||||||
// export const API_URL = 'https://thanksgiving2023.ngweb.io/api';
|
// export const API_URL = 'https://thanksgiving2023.ngweb.io/api';
|
||||||
export const WEBSOCK_URL = "https://thanksgiving2023.ngweb.io/"
|
//export const WEBSOCK_URL = "https://thanksgiving2023.ngweb.io/"
|
||||||
|
|
|
||||||
6
src/app/admin/admin-main/admin-main.component.html
Normal file
6
src/app/admin/admin-main/admin-main.component.html
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="actions m-2">
|
||||||
|
<h3>Game state</h3>
|
||||||
|
<app-main-actions>
|
||||||
|
</app-main-actions>
|
||||||
|
</div>
|
||||||
|
|
||||||
0
src/app/admin/admin-main/admin-main.component.scss
Normal file
0
src/app/admin/admin-main/admin-main.component.scss
Normal file
21
src/app/admin/admin-main/admin-main.component.spec.ts
Normal file
21
src/app/admin/admin-main/admin-main.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminMainComponent } from './admin-main.component';
|
||||||
|
|
||||||
|
describe('AdminMainComponent', () => {
|
||||||
|
let component: AdminMainComponent;
|
||||||
|
let fixture: ComponentFixture<AdminMainComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [AdminMainComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(AdminMainComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
10
src/app/admin/admin-main/admin-main.component.ts
Normal file
10
src/app/admin/admin-main/admin-main.component.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin-main',
|
||||||
|
templateUrl: './admin-main.component.html',
|
||||||
|
styleUrls: ['./admin-main.component.scss']
|
||||||
|
})
|
||||||
|
export class AdminMainComponent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ import { NgModule } from "@angular/core";
|
||||||
import { ActivatedRouteSnapshot, RouterModule, RouterStateSnapshot, Routes, UrlTree } from "@angular/router";
|
import { ActivatedRouteSnapshot, RouterModule, RouterStateSnapshot, Routes, UrlTree } from "@angular/router";
|
||||||
import { HomeComponent } from "./home/home.component";
|
import { HomeComponent } from "./home/home.component";
|
||||||
import { Observable, of } from "rxjs";
|
import { Observable, of } from "rxjs";
|
||||||
|
import {ConfigurationComponent} from "./configuration/configuration.component";
|
||||||
|
import {AdminMainComponent} from "./admin-main/admin-main.component";
|
||||||
|
|
||||||
export class AdminGuard {
|
export class AdminGuard {
|
||||||
|
|
||||||
|
|
@ -10,6 +12,9 @@ export class AdminGuard {
|
||||||
}
|
}
|
||||||
|
|
||||||
canDeactivate(component: HomeComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
canDeactivate(component: HomeComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||||
|
if(nextState?.url.indexOf('admin') !== -1){
|
||||||
|
return of(true);
|
||||||
|
}
|
||||||
return of(false);
|
return of(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,7 +26,19 @@ const routes: Routes = [
|
||||||
path: '',
|
path: '',
|
||||||
component: HomeComponent,
|
component: HomeComponent,
|
||||||
canDeactivate: [AdminGuard],
|
canDeactivate: [AdminGuard],
|
||||||
}
|
children: [
|
||||||
|
{
|
||||||
|
path:'',
|
||||||
|
component: AdminMainComponent,
|
||||||
|
canDeactivate: [AdminGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'configuration',
|
||||||
|
component: ConfigurationComponent,
|
||||||
|
canDeactivate: [AdminGuard],
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ import { CommonModule } from '@angular/common';
|
||||||
import { HomeComponent } from './home/home.component';
|
import { HomeComponent } from './home/home.component';
|
||||||
import { AdminRoutingModule } from "./admin-routing.module";
|
import { AdminRoutingModule } from "./admin-routing.module";
|
||||||
import { MainActionsComponent } from './components/main-actions/main-actions.component';
|
import { MainActionsComponent } from './components/main-actions/main-actions.component';
|
||||||
import { AppModule } from "../app.module";
|
|
||||||
import { SharedModule } from "../shared/shared.module";
|
import { SharedModule } from "../shared/shared.module";
|
||||||
import { QueueActionsComponent } from './components/queue-actions/queue-actions.component';
|
import { QueueActionsComponent } from './components/queue-actions/queue-actions.component';
|
||||||
|
import { ConfigurationComponent } from './configuration/configuration.component';
|
||||||
|
import { AdminNavComponent } from './components/admin-nav/admin-nav.component';
|
||||||
|
import { AdminMainComponent } from './admin-main/admin-main.component';
|
||||||
|
import { FeatureflagsComponent } from './components/featureflags/featureflags.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,7 +16,11 @@ import { QueueActionsComponent } from './components/queue-actions/queue-actions.
|
||||||
declarations: [
|
declarations: [
|
||||||
HomeComponent,
|
HomeComponent,
|
||||||
MainActionsComponent,
|
MainActionsComponent,
|
||||||
QueueActionsComponent
|
QueueActionsComponent,
|
||||||
|
ConfigurationComponent,
|
||||||
|
AdminNavComponent,
|
||||||
|
AdminMainComponent,
|
||||||
|
FeatureflagsComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule, AdminRoutingModule, SharedModule,
|
CommonModule, AdminRoutingModule, SharedModule,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
<a routerLink="/admin/">Main</a>
|
||||||
|
<a routerLink="/admin/configuration">Config</a>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
a:link, a:active, a:visited {
|
||||||
|
color: white;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AdminNavComponent } from './admin-nav.component';
|
||||||
|
|
||||||
|
describe('AdminNavComponent', () => {
|
||||||
|
let component: AdminNavComponent;
|
||||||
|
let fixture: ComponentFixture<AdminNavComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [AdminNavComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(AdminNavComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
10
src/app/admin/components/admin-nav/admin-nav.component.ts
Normal file
10
src/app/admin/components/admin-nav/admin-nav.component.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin-nav',
|
||||||
|
templateUrl: './admin-nav.component.html',
|
||||||
|
styleUrls: ['./admin-nav.component.scss']
|
||||||
|
})
|
||||||
|
export class AdminNavComponent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="m-2 featureflags">
|
||||||
|
<div class="form-group" *ngFor="let item of features">
|
||||||
|
<input class="form-check-input" type="checkbox" [checked]="item.state" [id]="item.name" (click)="setFeatureFlag(item.name)"/>
|
||||||
|
<label [for]="item.name">{{ item.name}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
.featureflags {
|
||||||
|
.form-group {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FeatureflagsComponent } from './featureflags.component';
|
||||||
|
|
||||||
|
describe('FeatureflagsComponent', () => {
|
||||||
|
let component: FeatureflagsComponent;
|
||||||
|
let fixture: ComponentFixture<FeatureflagsComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [FeatureflagsComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(FeatureflagsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
|
import {ApiService, FeatureFlagStateDto} from "../../../services/api.service";
|
||||||
|
import {FeatureFlagList} from "../../../shared/featureflags";
|
||||||
|
import {takeUntil} from "rxjs/operators";
|
||||||
|
import {Subject} from "rxjs";
|
||||||
|
import {EventService} from "../../../services/event.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-featureflags',
|
||||||
|
templateUrl: './featureflags.component.html',
|
||||||
|
styleUrls: ['./featureflags.component.scss']
|
||||||
|
})
|
||||||
|
export class FeatureflagsComponent implements OnInit, OnDestroy {
|
||||||
|
destroyed$ = new Subject<void>();
|
||||||
|
public features: FeatureFlagStateDto[] = [];
|
||||||
|
constructor(private apiService: ApiService, private eventService: EventService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.eventService.featureFlagChanged.pipe(takeUntil(this.destroyed$)).subscribe(result => this.loadFeatureFlags());
|
||||||
|
this.loadFeatureFlags();
|
||||||
|
}
|
||||||
|
private loadFeatureFlags() {
|
||||||
|
|
||||||
|
FeatureFlagList.FeatureFlags.map((featureFlag) => {
|
||||||
|
this.apiService.getFeatureFlagState(featureFlag).pipe(takeUntil(this.destroyed$)).subscribe((result) => {
|
||||||
|
if(!this.features.find((x) => x.name === result.name)) {
|
||||||
|
this.features.push(result);
|
||||||
|
} else {
|
||||||
|
const index = this.features.findIndex((x) => x.name === result.name);
|
||||||
|
this.features[index] = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeatureFlag(name: string) {
|
||||||
|
const ff = this.features.find((featureFlag) => featureFlag.name === name);
|
||||||
|
let newState = false;
|
||||||
|
if(ff) {
|
||||||
|
newState = !ff.state;
|
||||||
|
}
|
||||||
|
this.apiService.setFeatureFlagState(name, newState).pipe(takeUntil(this.destroyed$)).subscribe((result) => {
|
||||||
|
this.loadFeatureFlags();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/app/admin/configuration/configuration.component.html
Normal file
4
src/app/admin/configuration/configuration.component.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<div class="container-fluid mt-1">
|
||||||
|
<h3>FeatureFlags</h3>
|
||||||
|
<app-featureflags> </app-featureflags>
|
||||||
|
</div>
|
||||||
0
src/app/admin/configuration/configuration.component.scss
Normal file
0
src/app/admin/configuration/configuration.component.scss
Normal file
21
src/app/admin/configuration/configuration.component.spec.ts
Normal file
21
src/app/admin/configuration/configuration.component.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ConfigurationComponent } from './configuration.component';
|
||||||
|
|
||||||
|
describe('ConfigurationComponent', () => {
|
||||||
|
let component: ConfigurationComponent;
|
||||||
|
let fixture: ComponentFixture<ConfigurationComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ConfigurationComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(ConfigurationComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
10
src/app/admin/configuration/configuration.component.ts
Normal file
10
src/app/admin/configuration/configuration.component.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-configuration',
|
||||||
|
templateUrl: './configuration.component.html',
|
||||||
|
styleUrls: ['./configuration.component.scss']
|
||||||
|
})
|
||||||
|
export class ConfigurationComponent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<div class="container-fluid mt-1">
|
<div class="nav">
|
||||||
<app-main-actions>
|
<app-admin-nav></app-admin-nav>
|
||||||
|
</div>
|
||||||
</app-main-actions>
|
<router-outlet></router-outlet>
|
||||||
<app-queue-actions>
|
<div class="m-2">
|
||||||
|
<h3>Queue</h3>
|
||||||
</app-queue-actions>
|
<app-queue-actions>
|
||||||
</div>
|
</app-queue-actions>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import "../../../styles";
|
||||||
|
.nav {
|
||||||
|
background-color: $thg_orange;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
@import "../../../styles.scss";
|
@import "../../../styles.scss";
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
|
||||||
.card {
|
.card {
|
||||||
min-width: 150px;
|
min-width: 140px;
|
||||||
max-width: 150px;
|
max-width: 140px;
|
||||||
min-height: 230px;
|
min-height: 230px;
|
||||||
border: 0px solid #c2c2c2;
|
border: 0px solid #c2c2c2;
|
||||||
background: rgb(255,166,1);
|
background: rgb(255,166,1);
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
figure {
|
figure {
|
||||||
border-radius:100%;
|
border-radius:100%;
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 5px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<div class="participants-container" [ngClass]="{ 'small': small }">
|
<div class="participants-container" [ngClass]="{ 'small': small }">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
<div class="d-flex flex-row flex-wrap justify-content-center flex-nowrap" *ngIf="!small">
|
<div class="d-flex flex-row flex-wrap justify-content-center " *ngIf="!small">
|
||||||
<div *ngFor="let p of participants" >
|
<div *ngFor="let p of participants" >
|
||||||
<app-participant-item [small]="small" [banned]="p.banned" [bannedRemaining]="p.bannedRemaining" [participant]="p"></app-participant-item>
|
<app-participant-item [small]="small" [banned]="p.banned" [bannedRemaining]="p.bannedRemaining" [participant]="p"></app-participant-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap justify-content-center" *ngIf="small">
|
<div class="d-flex flex-wrap justify-content-center" *ngIf="small">
|
||||||
<div *ngFor="let p of participants">
|
<div *ngFor="let p of participants">
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ import { PenaltyDto } from "../../types/penalty.dto";
|
||||||
import { PrizeDto } from "../../types/prize.dto";
|
import { PrizeDto } from "../../types/prize.dto";
|
||||||
import {QuestionresultsDto} from "../../types/questionresults.dto";
|
import {QuestionresultsDto} from "../../types/questionresults.dto";
|
||||||
|
|
||||||
|
export interface FeatureFlagStateDto {
|
||||||
|
name: string;
|
||||||
|
state: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
|
|
@ -94,4 +99,12 @@ export class ApiService {
|
||||||
getQuestionResults() {
|
getQuestionResults() {
|
||||||
return this.httpClient.get<QuestionresultsDto[]>(`${API_URL}/quiz/question-results`)
|
return this.httpClient.get<QuestionresultsDto[]>(`${API_URL}/quiz/question-results`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFeatureFlagState(feature: string) {
|
||||||
|
return this.httpClient.get<FeatureFlagStateDto>(`${API_URL}/featureflag/${feature}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeatureFlagState(feature: string, state: boolean) {
|
||||||
|
return this.httpClient.post<FeatureFlagStateDto>(`${API_URL}/featureflag`, { name: feature, state: state });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export class EventService {
|
||||||
public gameResumed = new EventEmitter<ServerEvent<void>>();
|
public gameResumed = new EventEmitter<ServerEvent<void>>();
|
||||||
public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>();
|
public notificationEvent = new EventEmitter<ServerEvent<EventNotification>>();
|
||||||
public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>();
|
public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>();
|
||||||
|
public featureFlagChanged = new EventEmitter<ServerEvent<void>>();
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
public emit(event: ServerEvent<any>) {
|
public emit(event: ServerEvent<any>) {
|
||||||
|
|
@ -81,6 +82,9 @@ export class EventService {
|
||||||
case "user_property_changed":
|
case "user_property_changed":
|
||||||
this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>);
|
this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>);
|
||||||
break;
|
break;
|
||||||
|
case "feature_flag_changed":
|
||||||
|
this.featureFlagChanged.emit(event);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
src/app/shared/featureflags.ts
Normal file
3
src/app/shared/featureflags.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class FeatureFlagList {
|
||||||
|
static readonly FeatureFlags: string[] = ["EnableEndgamePoints"];
|
||||||
|
}
|
||||||
|
|
@ -87,5 +87,6 @@ export interface ServerEvent<T> {
|
||||||
| 'game_resumed'
|
| 'game_resumed'
|
||||||
| 'notification'
|
| 'notification'
|
||||||
| 'user_property_changed'
|
| 'user_property_changed'
|
||||||
|
| 'feature_flag_changed'
|
||||||
data: T
|
data: T
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue