diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 072271ee..96710356 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -24,6 +24,7 @@ import { IssueServiceFactory } from './core/services/factories/factory.issue.ser import { GithubService } from './core/services/github.service'; import { GithubEventService } from './core/services/githubevent.service'; import { IssueService } from './core/services/issue.service'; +import { LabelService } from './core/services/label.service'; import { LoggingService } from './core/services/logging.service'; import { PhaseService } from './core/services/phase.service'; import { SessionFixConfirmationComponent } from './core/services/session-fix-confirmation/session-fix-confirmation.component'; @@ -64,7 +65,17 @@ import { SharedModule } from './shared/shared.module'; { provide: AuthService, useFactory: AuthServiceFactory, - deps: [Router, NgZone, GithubService, UserService, IssueService, PhaseService, GithubEventService, Title, LoggingService] + deps: [ + Router, + NgZone, + GithubService, + UserService, + IssueService, + LabelService, + PhaseService, + GithubEventService, + Title, + LoggingService] }, { provide: IssueService, diff --git a/src/app/core/models/label.model.ts b/src/app/core/models/label.model.ts index ca02cbc0..18799a19 100644 --- a/src/app/core/models/label.model.ts +++ b/src/app/core/models/label.model.ts @@ -1,29 +1,30 @@ /** * Represents a label and its attributes. */ -export class Label { +export class Label implements SimpleLabel { readonly category: string; readonly name: string; + readonly formattedName: string; // 'category'.'name' (e.g. severity.Low) if a category exists or 'name' if the category does not exist. color: string; definition?: string; constructor(label: { name: string; color: string; definition?: string }) { const containsDotRegex = /\.\b/g; // contains dot in middle of name [this.category, this.name] = containsDotRegex.test(label.name) ? label.name.split('.') : [undefined, label.name]; + this.formattedName = this.category === undefined || this.category === '' ? this.name : this.category.concat('.', this.name); this.color = label.color; this.definition = label.definition; } - /** - * Returns the name of the label with the format of - * 'category'.'name' (e.g. severity.Low) if a category exists or - * 'name' if the category does not exist. - */ - public getFormattedName(): string { - return this.category === undefined || this.category === '' ? this.name : this.category.concat('.', this.name); - } - public equals(label: Label) { return this.name === label.name && this.category === label.category; } } + +/** + * Represents a simplified label with name and color + */ +export type SimpleLabel = { + formattedName: string; + color: string; +}; diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 645b8192..8b64537c 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -9,6 +9,7 @@ import { uuid } from '../../shared/lib/uuid'; import { GithubService } from './github.service'; import { GithubEventService } from './githubevent.service'; import { IssueService } from './issue.service'; +import { LabelService } from './label.service'; import { LoggingService } from './logging.service'; import { PhaseService } from './phase.service'; import { UserService } from './user.service'; @@ -42,6 +43,7 @@ export class AuthService { private githubService: GithubService, private userService: UserService, private issueService: IssueService, + private labelService: LabelService, private phaseService: PhaseService, private githubEventService: GithubEventService, private titleService: Title, @@ -67,6 +69,7 @@ export class AuthService { this.githubService.reset(); this.userService.reset(); this.issueService.reset(true); + this.labelService.reset(); this.phaseService.reset(); this.githubEventService.reset(); this.logger.reset(); diff --git a/src/app/core/services/factories/factory.auth.service.ts b/src/app/core/services/factories/factory.auth.service.ts index 2e9c762f..e3cf520d 100644 --- a/src/app/core/services/factories/factory.auth.service.ts +++ b/src/app/core/services/factories/factory.auth.service.ts @@ -6,6 +6,7 @@ import { AuthService } from '../auth.service'; import { GithubService } from '../github.service'; import { GithubEventService } from '../githubevent.service'; import { IssueService } from '../issue.service'; +import { LabelService } from '../label.service'; import { LoggingService } from '../logging.service'; // import { MockAuthService } from '../mocks/mock.auth.service'; import { PhaseService } from '../phase.service'; @@ -17,6 +18,7 @@ export function AuthServiceFactory( githubService: GithubService, userService: UserService, issueService: IssueService, + labelService: LabelService, phaseService: PhaseService, githubEventService: GithubEventService, titleService: Title, @@ -30,6 +32,7 @@ export function AuthServiceFactory( // githubService, // userService, // issueService, + // labelService, // phaseService, // githubEventService, // titleService, @@ -37,5 +40,15 @@ export function AuthServiceFactory( // ); // } console.log(logger); - return new AuthService(router, ngZone, githubService, userService, issueService, phaseService, githubEventService, titleService, logger); + return new AuthService( + router, + ngZone, + githubService, + userService, + issueService, + labelService, + phaseService, + githubEventService, + titleService, + logger); } diff --git a/src/app/core/services/label.service.ts b/src/app/core/services/label.service.ts index 77e107e4..edeac7d7 100644 --- a/src/app/core/services/label.service.ts +++ b/src/app/core/services/label.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { Label } from '../models/label.model'; +import { BehaviorSubject, EMPTY, Observable, of, Subscription, timer } from 'rxjs'; +import { catchError, exhaustMap, finalize, map } from 'rxjs/operators'; +import { Label, SimpleLabel } from '../models/label.model'; import { GithubService } from './github.service'; /* The threshold to decide if color is dark or light. @@ -22,10 +22,46 @@ const COLOR_WHITE = 'ffffff'; // Light color for text with dark background * from the GitHub repository for the WATcher application. */ export class LabelService { + static readonly POLL_INTERVAL = 5000; // 5 seconds + labels: Label[]; + simpleLabels: SimpleLabel[]; + + private labelsPollSubscription: Subscription; + private labelsSubject = new BehaviorSubject([]); constructor(private githubService: GithubService) {} + startPollLabels() { + if (this.labelsPollSubscription) { + return; + } + this.labelsPollSubscription = timer(0, LabelService.POLL_INTERVAL) + .pipe( + exhaustMap(() => { + return this.fetchLabels().pipe( + catchError(() => { + return EMPTY; + }) + ); + }) + ) + .subscribe(() => { + this.labelsSubject.next(this.simpleLabels); + }); + } + + stopPollLabels() { + if (this.labelsPollSubscription) { + this.labelsPollSubscription.unsubscribe(); + this.labelsPollSubscription = undefined; + } + } + + connect(): Observable { + return this.labelsSubject.asObservable(); + } + /** * Fetch labels from Github. */ @@ -33,6 +69,8 @@ export class LabelService { return this.githubService.fetchAllLabels().pipe( map((response) => { this.labels = this.parseLabelData(response); + this.simpleLabels = this.labels; + this.labelsSubject.next(this.simpleLabels); return response; }) ); @@ -92,4 +130,10 @@ export class LabelService { return styles; } + + reset() { + this.labels = undefined; + this.simpleLabels = undefined; + this.stopPollLabels(); + } } diff --git a/src/app/issues-viewer/issues-viewer.component.ts b/src/app/issues-viewer/issues-viewer.component.ts index af4890f9..0e399bcd 100644 --- a/src/app/issues-viewer/issues-viewer.component.ts +++ b/src/app/issues-viewer/issues-viewer.component.ts @@ -4,6 +4,7 @@ import { GithubUser } from '../core/models/github-user.model'; import { Repo } from '../core/models/repo.model'; import { GithubService } from '../core/services/github.service'; import { IssueService } from '../core/services/issue.service'; +import { LabelService } from '../core/services/label.service'; import { MilestoneService } from '../core/services/milestone.service'; import { PhaseService } from '../core/services/phase.service'; import { TABLE_COLUMNS } from '../shared/issue-tables/issue-tables-columns'; @@ -34,10 +35,12 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy { public phaseService: PhaseService, public githubService: GithubService, public issueService: IssueService, + public labelService: LabelService, public milestoneService: MilestoneService ) { this.repoChangeSubscription = this.phaseService.repoChanged$.subscribe((newRepo) => { this.issueService.reset(false); + this.labelService.reset(); this.initialize(); }); } diff --git a/src/app/shared/filter-bar/filter-bar.component.ts b/src/app/shared/filter-bar/filter-bar.component.ts index 13d00dcd..4d7ec511 100644 --- a/src/app/shared/filter-bar/filter-bar.component.ts +++ b/src/app/shared/filter-bar/filter-bar.component.ts @@ -91,9 +91,6 @@ export class FilterBarComponent implements OnInit, AfterViewInit, OnDestroy { * Fetch and initialize all information from repository to populate Issue Dashboard. */ private initialize() { - // Fetch labels - this.labelFilterBar.load(); - // Fetch milestones and update dropdown filter this.milestoneSubscription = this.milestoneService.fetchMilestones().subscribe( (response) => { diff --git a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html index e3f5c33c..3701bc6b 100644 --- a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html +++ b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.html @@ -16,12 +16,13 @@
No Labels Found!
+
- {{ label.name }} + {{ label.formattedName }}
diff --git a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts index 7261ec99..991a9d0d 100644 --- a/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts +++ b/src/app/shared/filter-bar/label-filter-bar/label-filter-bar.component.ts @@ -1,25 +1,22 @@ -import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatListOption, MatSelectionList } from '@angular/material/list'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { SimpleLabel } from '../../../core/models/label.model'; import { LabelService } from '../../../core/services/label.service'; import { LoggingService } from '../../../core/services/logging.service'; -export type simplifiedLabel = { - name: string; - color: string; -}; - @Component({ selector: 'app-label-filter-bar', templateUrl: './label-filter-bar.component.html', styleUrls: ['./label-filter-bar.component.css'] }) -export class LabelFilterBarComponent implements OnInit, OnDestroy { +export class LabelFilterBarComponent implements OnInit, AfterViewInit, OnDestroy { @Input() selectedLabels: BehaviorSubject; @Input() hiddenLabels: BehaviorSubject>; @ViewChild(MatSelectionList) matSelectionList; - allLabels: simplifiedLabel[]; + labels$: Observable; + allLabels: SimpleLabel[]; selectedLabelNames: string[] = []; hiddenLabelNames: Set = new Set(); loaded = false; @@ -30,7 +27,16 @@ export class LabelFilterBarComponent implements OnInit, OnDestroy { ngOnInit() { this.loaded = false; - this.load(); + } + + ngAfterViewInit(): void { + setTimeout(() => { + this.load(); + this.labels$ = this.labelService.connect(); + this.labels$.subscribe((labels) => { + this.allLabels = labels; + }); + }); } ngOnDestroy(): void { @@ -70,27 +76,18 @@ export class LabelFilterBarComponent implements OnInit, OnDestroy { /** loads in the labels in the repository */ public load() { + this.labelService.startPollLabels(); this.labelSubscription = this.labelService.fetchLabels().subscribe( (response) => { this.logger.debug('LabelFilterBarComponent: Fetched labels from Github'); }, (err) => {}, () => { - this.initialize(); + this.loaded = true; } ); } - private initialize() { - this.allLabels = this.labelService.labels.map((label) => { - return { - name: label.getFormattedName(), - color: label.color - }; - }); - this.loaded = true; - } - filter(filter: string, target: string): boolean { return !target.toLowerCase().includes(filter.toLowerCase()); } @@ -99,7 +96,7 @@ export class LabelFilterBarComponent implements OnInit, OnDestroy { if (this.allLabels === undefined || this.allLabels.length === 0) { return false; } - return this.allLabels.some((label) => !this.filter(filter, label.name)); + return this.allLabels.some((label) => !this.filter(filter, label.formattedName)); } updateSelection(): void { diff --git a/src/app/shared/layout/header.component.ts b/src/app/shared/layout/header.component.ts index 9f02c9f7..4a965fb0 100644 --- a/src/app/shared/layout/header.component.ts +++ b/src/app/shared/layout/header.component.ts @@ -11,6 +11,7 @@ import { ErrorHandlingService } from '../../core/services/error-handling.service import { GithubService } from '../../core/services/github.service'; import { GithubEventService } from '../../core/services/githubevent.service'; import { IssueService } from '../../core/services/issue.service'; +import { LabelService } from '../../core/services/label.service'; import { LoggingService } from '../../core/services/logging.service'; import { PhaseDescription, PhaseService } from '../../core/services/phase.service'; import { UserService } from '../../core/services/user.service'; @@ -46,6 +47,7 @@ export class HeaderComponent implements OnInit { private location: Location, private githubEventService: GithubEventService, private issueService: IssueService, + private labelService: LabelService, private errorHandlingService: ErrorHandlingService, private githubService: GithubService, private dialogService: DialogService @@ -85,6 +87,7 @@ export class HeaderComponent implements OnInit { // Remove current phase issues and load selected phase issues. this.githubService.reset(); this.issueService.reset(false); + this.labelService.reset(); this.reload(); // Route app to new phase. @@ -155,6 +158,13 @@ export class HeaderComponent implements OnInit { } ); + this.labelService.fetchLabels().subscribe( + (success) => success, + (error) => { + this.errorHandlingService.handleError(error, () => this.labelService.fetchLabels()); + } + ); + // Prevent user from spamming the reload button setTimeout(() => { this.isReloadButtonDisabled = false;