Skip to content

Commit

Permalink
v3.19.0-beta0 (#183)
Browse files Browse the repository at this point in the history
* Fix nav bar issue. Start adding external game hosts.

* Fixed a bug that made it impossible to add manual team bonuses if the team hadn't finished any challenges.

* More work on external hosts and unity cleanup

* Allow console names to contain team/challenge names if provided by querystring.

* More work on external hosts

* Renamed "unattempted" to "remaining" challenges on the scoreboard

* More work on deleting external hosts, gag.

* Fix some delete host bugs

* More styling for external hosts

* Cleanup of console title stuff

* Restyle new game controls slightly

* Misc cleanup

* Allow external + practice + sync start combos

* Disable practice share button if no search term

* Remove 'manager'. Improve invite ux.

* Add site usage report. no longer restrict external games to sync start in guards

* Prune a runaway scoreboard subscription

* Add loading indicator for clicking the ready button

* Add new team session start endpoint

* Display external host url on game admin page

* Change title of external game page

* Improve deployment admin styling. Add missing link to deployment in table view

* refactor to support individual team playability

* Add report descriptions, disable export for some reports.

* Finish site usage report

* Updates to site usage

* Changes to support deploy refactor

* Minor cleanup

* Improved handling for game hub events

* Minor player enroll ux updates

* add team ids property to game hub events

* Listen to all launch events on external game page.

* External loading bugs

* Fix launch progress game hub service bug

* Fix progress bars on external game load

* Add 'copy ticket comment'

* Add client info endpoint

* Add UI for predeploy in players menu

* Fix deploy resources event wireup

* Fix error rendering on external game page

* Fix bad api url

* Add loading indicator for session start

* Correct view logic for start session button

* Fix conflicting var name collision

* Change team external host url on the admin page to copy on click

* Slightly improve UX of invitation code generation
  • Loading branch information
sei-bstein authored May 16, 2024
1 parent 894d1d7 commit 90fa4f1
Show file tree
Hide file tree
Showing 112 changed files with 1,480 additions and 1,107 deletions.
2 changes: 2 additions & 0 deletions projects/gameboard-mks/src/app/api.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface ConsoleRequest {
observer?: boolean; // Whether this challenge is being observed (o=0 or o=1)
userId?: string; // The hexadecimal ID of the user trying to access the VM (u=e3c...)
enableActivityListener: boolean; // Indicates whether the user activity listener should be enabled. Only works for practice consoles.
challengeName?: string; // the name of the challenge which owns the console
teamName?: string; // the name of the player/team using the console
}

export interface ConsolePresence {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,19 @@ export class ConsoleComponent implements AfterViewInit, OnDestroy {
this.canvasId = el.id + this.index;
el.id += this.index;

if (!!this.request?.name) {
this.titleSvc.setTitle(`console: ${this.request.name}`);
}
let teamNameBit = "";
if (this.request.teamName)
teamNameBit = `${this.request.teamName} on`

let onConsoleBit = "console";
if (this.request?.name)
onConsoleBit = this.request.name;

let challengeNameBit = "";
if (this.request.challengeName)
challengeNameBit = ` :: ${this.request.challengeName}`;

this.titleSvc.setTitle(teamNameBit + onConsoleBit + challengeNameBit);

if (!!this.request.observer) {
this.showCog = false;
Expand All @@ -102,8 +112,6 @@ export class ConsoleComponent implements AfterViewInit, OnDestroy {
} else {
setTimeout(() => this.reload(), 1);
}
// TODO: restore audience hub
// setTimeout(() => this.hubSvc.init(this.request), 100);

this.audienceDiv.nativeElement.onmousedown = (e: MouseEvent) => {
e.preventDefault();
Expand Down
9 changes: 6 additions & 3 deletions projects/gameboard-ui/src/app/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import { PlayerSponsorReportComponent } from './player-sponsor-report/player-spo
import { PracticeComponent } from './practice/practice.component';
import { PracticeSettingsComponent } from './practice/practice-settings/practice-settings.component';
import { PrereqsComponent } from './prereqs/prereqs.component';
import { ReportPageComponent } from './report-page/report-page.component';
import { SiteOverviewStatsComponent } from './components/site-overview-stats/site-overview-stats.component';
import { SpecBrowserComponent } from './spec-browser/spec-browser.component';
import { SponsorBrowserComponent } from './sponsor-browser/sponsor-browser.component';
Expand All @@ -64,6 +63,9 @@ import { ActiveTeamsModalComponent } from './components/active-teams-modal/activ
import { AdminEnrollTeamModalComponent } from './components/admin-enroll-team-modal/admin-enroll-team-modal.component';
import { GameYamlImportModalComponent } from './components/game-yaml-import-modal/game-yaml-import-modal.component';
import { SyncStartGameStateDescriptionPipe } from './pipes/sync-start-game-state-description.pipe';
import { ExternalGameHostPickerComponent } from './components/external-game-host-picker/external-game-host-picker.component';
import { ExternalHostEditorComponent } from './components/external-host-editor/external-host-editor.component';
import { DeleteExternalGameHostModalComponent } from './components/delete-external-game-host-modal/delete-external-game-host-modal.component';

@NgModule({
declarations: [
Expand Down Expand Up @@ -98,7 +100,6 @@ import { SyncStartGameStateDescriptionPipe } from './pipes/sync-start-game-state
PracticeComponent,
PracticeSettingsComponent,
PrereqsComponent,
ReportPageComponent,
SpecBrowserComponent,
SponsorBrowserComponent,
SupportReportLegacyComponent,
Expand All @@ -117,6 +118,9 @@ import { SyncStartGameStateDescriptionPipe } from './pipes/sync-start-game-state
GameYamlImportModalComponent,
SyncStartTeamPlayerReadyCountPipe,
SyncStartGameStateDescriptionPipe,
ExternalGameHostPickerComponent,
ExternalHostEditorComponent,
DeleteExternalGameHostModalComponent,
],
imports: [
CommonModule,
Expand All @@ -142,7 +146,6 @@ import { SyncStartGameStateDescriptionPipe } from './pipes/sync-start-game-state
{ path: 'observer/challenges/:id', component: ChallengeObserverComponent, title: "Admin | Observe" },
{ path: 'observer/teams/:id', component: TeamObserverComponent },
{ path: 'overview', component: AdminOverviewComponent, title: "Admin | Overview" },
{ path: 'report', component: ReportPageComponent, title: "Admin | Reports" },
{ path: 'report/users', component: UserReportComponent },
{ path: 'report/sponsors', component: PlayerSponsorReportComponent },
{ path: 'report/challenges', component: ChallengeReportComponent },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ <h4 class="col-2 text-right">Consoles</h4>
</div> <!-- end users list header -->
<div class="iframe-wrapper mt-auto" style="padding: 0px;">
<iframe class="rounded-bottom w-100 h-100" frameborder="0"
[src]="(mksHost+'?f=0&o=1&s='+vm.challengeId+'&v='+vm.name) | safeurl">
[src]="(mksHost+'?f=0&o=1&s='+vm.challengeId+'&v='+vm.name+'&teamName='+row.value.playerName+'&challengeName='+row.value.name) | safeurl">
</iframe>
</div> <!-- end iframe wrapper -->
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class AdminOverviewComponent {
protected supportHubState$: Observable<HubConnectionState>;

constructor(
private gameHubService: GameHubService,
gameHubService: GameHubService,
supportHub: SupportHubService) {
this.gameHubState$ = gameHubService.hubState$;
this.supportHubState$ = supportHub.hubState$;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<app-modal-content *ngIf="deleteHost; else loading" title="Delete External Host" confirmButtonText="Yes, delete"
cancelButtonText="No, don't delete" (confirm)="handleConfirm(replaceHost)" [isDangerConfirm]="true">
<app-error-div [errors]="errors"></app-error-div>
<div class="used-by-games-container" *ngIf="deleteHost.usedByGames.length">
This external game host is in use{{ deleteHost.usedByGames.length === 1 ? " by the game " +
deleteHost!.usedByGames[0].name : "" }}. To delete it, you'll need to transfer the games
it's currently hosting to another host.

<ul *ngIf="deleteHost.usedByGames.length > 1">
<li *ngFor="let game of deleteHost.usedByGames">{{game.name}}</li>
</ul>
</div>
<div class="form-group">
<label for="target-host">Transfer these games to host</label>
<select id="target-host" name="target-host" class="form-control" [(ngModel)]="replaceHost">
<option *ngFor="let host of hosts" [ngValue]="host">{{host.name}} ({{host.hostUrl}})</option>
</select>
</div>
</app-modal-content>

<ng-template #loading>
<app-spinner>Loading the host...</app-spinner>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ExternalGameHost } from '@/api/game-models';
import { ExternalGameService } from '@/services/external-game.service';
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-delete-external-game-host-modal',
templateUrl: './delete-external-game-host-modal.component.html',
styleUrls: ['./delete-external-game-host-modal.component.scss']
})
export class DeleteExternalGameHostModalComponent implements OnInit {
deleteHostId?: string;
deleted?: (migratedToHostId: string) => void | Promise<void>;

protected deleteHost?: ExternalGameHost;
protected replaceHost?: ExternalGameHost;
protected errors: any[] = [];
protected hosts: ExternalGameHost[] = [];

constructor(private externalGameService: ExternalGameService) { }

async ngOnInit(): Promise<void> {
if (!this.deleteHostId)
return;

const response = await this.externalGameService.getHosts();

if (response.hosts.length === 0)
this.errors.push("No external hosts configured.");

this.hosts = response.hosts.filter(h => h.id != this.deleteHostId);
this.deleteHost = response.hosts.find(h => h.id === this.deleteHostId);
this.replaceHost = response.hosts[0];
}

protected async handleConfirm(replaceHost?: ExternalGameHost) {
this.errors = [];

if (!replaceHost)
this.errors.push("Couldn't resolve a replacement host.");

if (this.deleted)
try {
await this.deleted(replaceHost!.id);
}
catch (err) {
this.errors.push(err);
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ <h4 class="card-title flex-grow w-100">
</div>

<div class="card-text mb-4">
<div class="card-section my-4">
<div class="card-section">
<h5 class="mb-3">
Players
<span class="player-count">({{ team.players | syncStartTeamPlayerReadyCount }}
Expand Down Expand Up @@ -118,9 +118,14 @@ <h5>Challenges</h5>
</tr>
</tbody>
</table>

</div>

<div class="card-section" *ngIf="team.externalGameHostUrl">
<h5>Game Host URL</h5>

<div class="cursor-pointer text-info" [appCopyOnClick]="team.externalGameHostUrl"
appCopyOnClickMessage="Copied this team's external host URL to your clipboard."></div>
</div>
</div>
<button type="button" href="#" class="btn btn-info"
[disabled]="(team.deployStatus != 'notStarted' && team.deployStatus != 'partiallyDeployed') || !canDeploy"
Expand All @@ -143,8 +148,8 @@ <h5>Challenges</h5>

<ng-template #loading>
<div class="w-100 d-flex align-items-center justify-content-center">
<app-spinner>
<h1>Loading game data...</h1>
<app-spinner textPosition="bottom">
Loading game data...
</app-spinner>
</div>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ h5 {
}
}

.card-section {
margin: 32px 0 16px;
}

.team-card {
flex-basis: 48%;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ExternalGameDeployStatus } from '@/api/game-models';
import { SimpleEntity, SimpleSponsor } from '@/api/models';
import { Subject, combineLatest, debounceTime, filter, firstValueFrom, map, timer } from 'rxjs';
import { DateTime } from 'luxon';
import { SimpleEntity, SimpleSponsor } from '@/api/models';
import { fa } from '@/services/font-awesome.service';
import { AppTitleService } from '@/services/app-title.service';
import { UnsubscriberService } from '@/services/unsubscriber.service';
import { Subject, combineLatest, debounceTime, filter, firstValueFrom, map, timer } from 'rxjs';
import { ExternalGameService } from '@/services/external-game.service';
import { ActivatedRoute } from '@angular/router';
import { FriendlyDatesService } from '@/services/friendly-dates.service';

export type SyncStartPlayerStatus = "notConnected" | "notReady" | "ready";
Expand All @@ -17,11 +17,12 @@ export interface ExternalGameAdminTeam {
name: string;
sponsors: SimpleSponsor[];
deployStatus: ExternalGameDeployStatus;
externalGameHostUrl?: string;
isReady: boolean;
players: {
id: string;
name: string;
isManager: boolean;
isCaptain: boolean;
sponsor: SimpleSponsor;
status: SyncStartPlayerStatus;
user: SimpleEntity;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<select id="external-host-picker" *ngIf="hosts.length; else noHosts" name="external-host-picker" #hostsSelect
class="form-control d-block" [(ngModel)]="selectedHost" (ngModelChange)="handleHostSelect(selectedHost)">
<option *ngFor="let host of hosts" [ngValue]="host">
{{host.name}} ({{ host.hostUrl }})
</option>
</select>
</div>

<div class="btn-group ml-2">
<button *ngIf="selectedHostId" class="btn btn-danger" (click)="handleDeleteClick(selectedHostId)"
[disabled]="hosts.length <= 1" tooltip="Delete this host">
<fa-icon [icon]="fa.trash"></fa-icon>
</button>
<button *ngIf="selectedHostId" class="btn btn-info" (click)="handleEditClick(selectedHostId)"
tooltip="Edit this host's settings">
<fa-icon [icon]="fa.edit"></fa-icon>
</button>
<button class="btn btn-info" (click)="handleAddClick()" tooltip="Create a new external host">
<fa-icon [icon]="fa.plus"></fa-icon>
</button>
</div>
</div>

<ng-template #noHosts>
<span class="text-muted">
No external hosts have been created. You'll need to <button type="text" class="btn btn-link text-info px-0"
(click)="handleAddClick()">add one</button> to configure this external game.
</span>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.btn-group {
button {
border-left: solid 1px white;
}

:first-child {
border-left: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core';
import { ExternalGameHost, UpsertExternalGameHost } from '@/api/game-models';
import { fa } from "@/services/font-awesome.service";
import { ModalConfirmService } from '@/services/modal-confirm.service';
import { ExternalHostEditorComponent } from '../external-host-editor/external-host-editor.component';
import { ExternalGameService } from '@/services/external-game.service';
import { DeleteExternalGameHostModalComponent } from '../delete-external-game-host-modal/delete-external-game-host-modal.component';

@Component({
selector: 'app-external-game-host-picker',
templateUrl: './external-game-host-picker.component.html',
styleUrls: ['./external-game-host-picker.component.scss']
})
export class ExternalGameHostPickerComponent implements OnInit {
@Input() selectedHostId?: string;
@Output() selectedHostIdChange = new EventEmitter<string>();
@Output() selectedHostChange = new EventEmitter<ExternalGameHost>();

protected fa = fa;
protected hosts: ExternalGameHost[] = [];
protected selectedHost?: ExternalGameHost;

constructor(
private externalGameService: ExternalGameService,
private modalService: ModalConfirmService) { }

async ngOnInit(): Promise<void> {
await this.loadHosts(this.selectedHostId);
}

ngOnChanges(changes: SimpleChanges) {
if (changes.selectedHostId && !changes.selectedHostId.firstChange) {
this.selectedHost = this.hosts.find(h => h.id === changes.selectedHostId.currentValue);
}
}

protected async handleAddClick() {
this.modalService.openComponent({
content: ExternalHostEditorComponent,
context: {
onSave: async (host) => await this.handleSave(host)
},
},);
}

protected handleDeleteClick(hostId: string) {
this.modalService.openComponent({
content: DeleteExternalGameHostModalComponent,
context: {
deleteHostId: hostId,
deleted: async migratedToHostId => {
await this.externalGameService.deleteHost(hostId, migratedToHostId);
await this.loadHosts(migratedToHostId);
}
},
});
}

protected handleEditClick(hostId: string) {
this.modalService.openComponent({
content: ExternalHostEditorComponent,
context: {
hostId: hostId,
onSave: async (host) => await this.handleSave(host)
}
});
}

protected handleHostSelect(selectedHost?: ExternalGameHost) {
if (!selectedHost?.id)
return;

this.selectedHostId = selectedHost.id;
this.selectedHost = selectedHost;
this.selectedHostIdChange.emit(selectedHost.id);
this.selectedHostChange.emit(selectedHost);
}

private async handleSave(host: UpsertExternalGameHost) {
await this.externalGameService.upsertExternalGameHost(host);
await this.loadHosts(host.id);
}

private async loadHosts(selectedHostId?: string) {
const response = await this.externalGameService.getHosts();
this.hosts = response?.hosts || [];

if (selectedHostId) {
this.handleHostSelect(this.hosts.find(h => h.id === selectedHostId));
}
}
}
Loading

0 comments on commit 90fa4f1

Please sign in to comment.