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 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 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 { HomeComponent } from "./home/home.component";
|
||||
import { Observable, of } from "rxjs";
|
||||
import {ConfigurationComponent} from "./configuration/configuration.component";
|
||||
import {AdminMainComponent} from "./admin-main/admin-main.component";
|
||||
|
||||
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 {
|
||||
if(nextState?.url.indexOf('admin') !== -1){
|
||||
return of(true);
|
||||
}
|
||||
return of(false);
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +26,19 @@ const routes: Routes = [
|
|||
path: '',
|
||||
component: HomeComponent,
|
||||
canDeactivate: [AdminGuard],
|
||||
}
|
||||
children: [
|
||||
{
|
||||
path:'',
|
||||
component: AdminMainComponent,
|
||||
canDeactivate: [AdminGuard],
|
||||
},
|
||||
{
|
||||
path: 'configuration',
|
||||
component: ConfigurationComponent,
|
||||
canDeactivate: [AdminGuard],
|
||||
}]
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { CommonModule } from '@angular/common';
|
|||
import { HomeComponent } from './home/home.component';
|
||||
import { AdminRoutingModule } from "./admin-routing.module";
|
||||
import { MainActionsComponent } from './components/main-actions/main-actions.component';
|
||||
import { AppModule } from "../app.module";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
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: [
|
||||
HomeComponent,
|
||||
MainActionsComponent,
|
||||
QueueActionsComponent
|
||||
QueueActionsComponent,
|
||||
ConfigurationComponent,
|
||||
AdminNavComponent,
|
||||
AdminMainComponent,
|
||||
FeatureflagsComponent,
|
||||
],
|
||||
imports: [
|
||||
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">
|
||||
<app-main-actions>
|
||||
|
||||
</app-main-actions>
|
||||
<app-queue-actions>
|
||||
|
||||
</app-queue-actions>
|
||||
</div>
|
||||
<div class="nav">
|
||||
<app-admin-nav></app-admin-nav>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
<div class="m-2">
|
||||
<h3>Queue</h3>
|
||||
<app-queue-actions>
|
||||
</app-queue-actions>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
@import "../../../styles";
|
||||
.nav {
|
||||
background-color: $thg_orange;
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
@import "../../../styles.scss";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Pacifico&display=swap');
|
||||
.card {
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
min-width: 140px;
|
||||
max-width: 140px;
|
||||
min-height: 230px;
|
||||
border: 0px solid #c2c2c2;
|
||||
background: rgb(255,166,1);
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
figure {
|
||||
border-radius:100%;
|
||||
display:inline-block;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<div class="participants-container" [ngClass]="{ 'small': small }">
|
||||
<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" >
|
||||
<app-participant-item [small]="small" [banned]="p.banned" [bannedRemaining]="p.bannedRemaining" [participant]="p"></app-participant-item>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-center" *ngIf="small">
|
||||
<div *ngFor="let p of participants">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ import { PenaltyDto } from "../../types/penalty.dto";
|
|||
import { PrizeDto } from "../../types/prize.dto";
|
||||
import {QuestionresultsDto} from "../../types/questionresults.dto";
|
||||
|
||||
export interface FeatureFlagStateDto {
|
||||
name: string;
|
||||
state: boolean;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
|
|
@ -94,4 +99,12 @@ export class ApiService {
|
|||
getQuestionResults() {
|
||||
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 notificationEvent = new EventEmitter<ServerEvent<EventNotification>>();
|
||||
public userPropertyChanged = new EventEmitter<ServerEvent<UserPropertyChanged>>();
|
||||
public featureFlagChanged = new EventEmitter<ServerEvent<void>>();
|
||||
constructor() { }
|
||||
|
||||
public emit(event: ServerEvent<any>) {
|
||||
|
|
@ -81,6 +82,9 @@ export class EventService {
|
|||
case "user_property_changed":
|
||||
this.userPropertyChanged.emit(event as ServerEvent<UserPropertyChanged>);
|
||||
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'
|
||||
| 'notification'
|
||||
| 'user_property_changed'
|
||||
| 'feature_flag_changed'
|
||||
data: T
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue