feature flag support

This commit is contained in:
Kirill Ivlev 2024-11-11 17:21:42 +04:00
parent dd9932d2db
commit 3bb63d1d5a
27 changed files with 264 additions and 18 deletions

View file

@ -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/"

View file

@ -0,0 +1,6 @@
<div class="actions m-2">
<h3>Game state</h3>
<app-main-actions>
</app-main-actions>
</div>

View 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();
});
});

View 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 {
}

View file

@ -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({

View file

@ -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,

View file

@ -0,0 +1,3 @@
<a routerLink="/admin/">Main</a>
<a routerLink="/admin/configuration">Config</a>

View file

@ -0,0 +1,4 @@
a:link, a:active, a:visited {
color: white;
padding: 3px;
}

View file

@ -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();
});
});

View 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 {
}

View file

@ -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>

View file

@ -0,0 +1,8 @@
.featureflags {
.form-group {
margin-left: 5px;
}
label {
margin-left: 3px;
}
}

View file

@ -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();
});
});

View file

@ -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();
});
}
}

View file

@ -0,0 +1,4 @@
<div class="container-fluid mt-1">
<h3>FeatureFlags</h3>
<app-featureflags> </app-featureflags>
</div>

View 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();
});
});

View 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 {
}

View file

@ -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>

View file

@ -0,0 +1,5 @@
@import "../../../styles";
.nav {
background-color: $thg_orange;
}

View file

@ -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;
}

View file

@ -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">

View file

@ -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 });
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,3 @@
export class FeatureFlagList {
static readonly FeatureFlags: string[] = ["EnableEndgamePoints"];
}

View file

@ -87,5 +87,6 @@ export interface ServerEvent<T> {
| 'game_resumed'
| 'notification'
| 'user_property_changed'
| 'feature_flag_changed'
data: T
}