Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/app/core/constants/milestone-anomalies.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum MilestoneAnomaliesStatus {
NoDeadline = 'No Deadline',
PastDeadline = 'Past Deadline',
ClosedMilestoneAnomaly = 'Closed milestone with open issues or unmerged pull requests',
MultipleOpenMilestoneAnomaly = 'Multiple milestones are left open'
}
13 changes: 13 additions & 0 deletions src/app/core/models/anomaly.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Represents an anomaly state of the objects used in the app (e.g. Milestones, Issues etc).
// Should not be used to represent errors that affects normal functionality of the app.

export abstract class Anomaly {
abstract readonly anomalyType: string;
anomaly: string;

constructor(anomaly: string) {
this.anomaly = anomaly;
}

abstract getDescription(): string;
}
40 changes: 40 additions & 0 deletions src/app/core/models/milestone-anomaly.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Anomaly } from './anomaly.model';
import { Milestone } from './milestone.model';

export abstract class MilestoneAnomaly extends Anomaly {
readonly anomalyType = 'MilestoneAnomaly';

constructor(anomaly: string) {
super(anomaly);
}
}

// Represents an anomaly that is related to a single milestone
export class SingleMilestoneAnomaly extends MilestoneAnomaly {
milestone: Milestone;

constructor(milestone: Milestone, anomaly: string) {
super(anomaly);
this.milestone = milestone;
}

getDescription(): string {
return `${this.milestone.title}: ${this.anomaly}`;
}
}

// Represents an anomaly that is related to multiple milestones
export class GeneralMilestoneAnomaly extends MilestoneAnomaly {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the only difference between this class and the SingleMilestoneAnomaly class is that this holds multiple milestones instead of one, perhaps can call this MultipleMilestoneAnomaly instead? Do correct me if I'm understanding this wrongly!

milestones: Milestone[];

constructor(milestones: Milestone[], anomaly: string) {
super(anomaly);
this.milestones = milestones;
}

getDescription(): string {
const milestoneTitles = this.milestones.map((milestone) => milestone.title).join(', ');

return `${this.anomaly}: ${milestoneTitles}`;
}
}
123 changes: 123 additions & 0 deletions src/app/core/services/milestone.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { replaceNewlinesWithBreakLines } from '../../shared/lib/html';
import { MilestoneAnomaliesStatus } from '../constants/milestone-anomalies.constants';
import { Issue } from '../models/issue.model';
import { GeneralMilestoneAnomaly, MilestoneAnomaly, SingleMilestoneAnomaly } from '../models/milestone-anomaly.model';
import { Milestone } from '../models/milestone.model';
import { GithubService } from './github.service';
import { IssueService } from './issue.service';

@Injectable({
providedIn: 'root'
Expand All @@ -15,6 +21,7 @@ import { GithubService } from './github.service';
export class MilestoneService {
milestones: Milestone[] = [];
hasNoMilestones: boolean;
milestoneAnomalies: MilestoneAnomaly[] = [];

constructor(private githubService: GithubService) {}

Expand All @@ -26,6 +33,7 @@ export class MilestoneService {
map((response) => {
this.milestones = this.parseMilestoneData(response);
this.hasNoMilestones = response.length === 0;
this.updateMilestoneAnomalies();
return response;
})
);
Expand Down Expand Up @@ -98,4 +106,119 @@ export class MilestoneService {
}
return latestClosedMilestone;
}

/**
* Updates the MilestoneAnomalies associated with this milestoneService.
* This does not check for closed milestone with open issues or unmerged PR.
*/
updateMilestoneAnomalies() {
// removes all previous milestone anomalies first.
this.milestoneAnomalies = [];

this.checkMilestoneDeadlineAnomalies();
this.checkMultipleOpenedMilestonesAnomaly();
}

getMilestoneAnomalies(): MilestoneAnomaly[] {
return this.milestoneAnomalies;
}

/**
* Checks if the milestones have any anomalies.
*/
hasMilestoneAnomalies(): boolean {
return this.milestoneAnomalies.length > 0;
}

/**
* Checks whether a milestone has a deadline and whether the milestone has gone past deadline.
*/
checkMilestoneDeadlineAnomalies() {
for (const milestone of this.milestones) {
if (!milestone.deadline) {
// Milestone has no deadline
const newAnomaly = new SingleMilestoneAnomaly(milestone, MilestoneAnomaliesStatus.NoDeadline);
this.milestoneAnomalies.push(newAnomaly);
} else if (milestone.state === 'open' && milestone.deadline < new Date()) {
// Milestone deadline has past
const newAnomaly = new SingleMilestoneAnomaly(milestone, MilestoneAnomaliesStatus.PastDeadline);
this.milestoneAnomalies.push(newAnomaly);
}
}
}

/**
* Checks for milestone anomalies related to milestone issues and PRs.
*/
checkMilestoneIssuesAnomalies(issueService: IssueService) {
issueService.issues$.subscribe((issues: Issue[]) => {
this.updateClosedMilestoneWithOpenIssueOrPR(issues);
});
}

/**
* Checks if a closed milestone has open or unmerged PRs
* Loops through the issues, checks if the issue/PR is still open but the corresponding milestone is closed.
* Since this method is called from BehaviorSubject, to prevent adding of multiple copies of the same anomaly,
* filter the current milestone anomalies to remove all anomalies related to closed milestone anomalies with open issues/PRs
* first.
*/
updateClosedMilestoneWithOpenIssueOrPR(issues: Issue[]) {
// Filtering the milestone anomalies.
this.milestoneAnomalies = this.milestoneAnomalies.filter((milestoneAnomaly) => {
return milestoneAnomaly.anomaly !== MilestoneAnomaliesStatus.ClosedMilestoneAnomaly;
});

const milestonesWithAnomaly: Set<Milestone> = new Set<Milestone>();
for (const issue of issues) {
const issueMilestone: Milestone = issue.milestone;
if (issue.state === 'OPEN' && issueMilestone && issueMilestone.state === 'CLOSED') {
milestonesWithAnomaly.add(issueMilestone);
}
}
for (const milestone of milestonesWithAnomaly) {
const newAnomaly = new SingleMilestoneAnomaly(milestone, MilestoneAnomaliesStatus.ClosedMilestoneAnomaly);
this.milestoneAnomalies.push(newAnomaly);
}
}

/**
* Checks if there are multiple open milestones.
*/
checkMultipleOpenedMilestonesAnomaly() {
const allOpenMilestones: Milestone[] = [];
for (const milestone of this.milestones) {
if (milestone.state === 'open') {
allOpenMilestones.push(milestone);
}
}
if (allOpenMilestones.length > 1) {
// there are multiple open milestones.
const newAnomaly = new GeneralMilestoneAnomaly(allOpenMilestones, MilestoneAnomaliesStatus.MultipleOpenMilestoneAnomaly);
this.milestoneAnomalies = [newAnomaly, ...this.milestoneAnomalies];
}
}

/**
* Returns a string that contains all the current milestone anomalies.
* New line is created using `\n` symbols.
* When display using HTML, `\n` needs to be replaced with `<br>`.
*/
getConsolidatedAnomaliesMessage(): string {
let message = null;
if (!this.hasMilestoneAnomalies) {
return message;
}

const milestoneAnomalies = this.getMilestoneAnomalies();
message = milestoneAnomalies.map((milestoneAnomaly) => `${milestoneAnomaly.getDescription()}`).join('\n');
return message;
}

/**
* Returns the anomalies message to be displayed using HTML
*/
getConsolidatedAnomaliesMessageHTML(): string {
return replaceNewlinesWithBreakLines(this.getConsolidatedAnomaliesMessage());
}
}
53 changes: 53 additions & 0 deletions src/app/issues-viewer/issues-viewer.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,56 @@
box-sizing: border-box;
padding: 0.5rem 1rem 0.5rem 0;
}

.main-content {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
height: 100%;
}

.warning-card {
max-height: 200px;
display: flex;
flex-direction: column;
align-items: start;
background: #fff8e1;
border-left: 5px solid #fbc02d;
padding: 12px;
margin: 12px;
}

.warning-card .header {
display: flex;
align-items: center;
margin-bottom: 8px;
flex: 1;
max-height: 20px;
width: 100%;
}

.warning-card .mat-icon {
color: #fbc02d;
}

.warning-card .title {
flex: 1;
margin-left: 12px;
font-weight: bold;
}

.warning-card.button {
align-self: flex-start;
}

.warning-card .content {
overflow-y: auto;
flex: 1;
margin-left: 12px;
font-size: 14px;
max-height: 100px;
line-height: 1.5;
scrollbar-width: thin;
scrollbar-color: #fbc02d transparent;
}
43 changes: 27 additions & 16 deletions src/app/issues-viewer/issues-viewer.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
<mat-drawer-container class="drawer-container">
<mat-drawer #sidenav mode="side" opened>
<div class="left">
<app-filter-bar
[views$]="views"
(escapePressed)="sidenav.close()"
>
</app-filter-bar>
<app-filter-bar [views$]="views" (escapePressed)="sidenav.close()"> </app-filter-bar>
</div>
</mat-drawer>
<mat-drawer-content>
Expand All @@ -25,17 +21,32 @@
>chevron_right</mat-icon
>
</div>
<div class="right">
<app-card-view
*ngFor="let group of this.groupService.groups"
class="issue-table"
#card
[ngStyle]="{ display: card.isLoading || card.issueLength > 0 ? 'initial' : 'none' }"
[group]="group"
[headers]="this.displayedColumns"
(issueLengthChange)="this.groupService.updateHiddenGroups($event, group)"
></app-card-view>
<app-hidden-groups [groups]="this.groupService.hiddenGroups"></app-hidden-groups>
<div class="main-content">
<mat-card class="warning-card" *ngIf="this.isDisplayMilestoneAnomalies()">
<div class="header">
<mat-icon color="warn">warning</mat-icon>
<div class="title">Milestone Anomalies:</div>
<button mat-icon-button (click)="dismissMilestoneAnomalies()">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="content">
<div [innerHTML]="this.milestoneService.getConsolidatedAnomaliesMessageHTML()"></div>
</div>
</mat-card>

<div class="right">
<app-card-view
*ngFor="let group of this.groupService.groups"
class="issue-table"
#card
[ngStyle]="{ display: card.isLoading || card.issueLength > 0 ? 'initial' : 'none' }"
[group]="group"
[headers]="this.displayedColumns"
(issueLengthChange)="this.groupService.updateHiddenGroups($event, group)"
></app-card-view>
<app-hidden-groups [groups]="this.groupService.hiddenGroups"></app-hidden-groups>
</div>
</div>
</div>
</mat-drawer-content>
Expand Down
25 changes: 24 additions & 1 deletion src/app/issues-viewer/issues-viewer.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { filter } from 'rxjs/operators';
import { FiltersService } from '../core/services/filters.service';
import { GithubService } from '../core/services/github.service';
import { GroupService } from '../core/services/grouping/group.service';
import { GroupingContextService } from '../core/services/grouping/grouping-context.service';
import { GroupBy, GroupingContextService } from '../core/services/grouping/grouping-context.service';
import { IssueService } from '../core/services/issue.service';
import { LabelService } from '../core/services/label.service';
import { MilestoneService } from '../core/services/milestone.service';
Expand Down Expand Up @@ -42,6 +42,9 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy {
/** Hide or show the filter bar */
showFilterBar = false;

// Hide or display milestone anomalies
showMilestoneAnomalies = true;

constructor(
public viewService: ViewService,
public githubService: GithubService,
Expand Down Expand Up @@ -103,9 +106,29 @@ export class IssuesViewerComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.groupService.resetGroups();
this.availableGroupsSubscription = this.groupingContextService.getGroups().subscribe((x) => (this.groupService.groups = x));
this.showMilestoneAnomalies = true;
this.milestoneService.checkMilestoneIssuesAnomalies(this.issueService);
}

private toggleSidebar() {
this.showFilterBar = !this.showFilterBar;
}

private dismissMilestoneAnomalies() {
this.showMilestoneAnomalies = false;
}

/**
* Checks if the current GroupBy is by milestone.
*/
private isGroupByMilestone() {
return this.groupingContextService.currGroupBy === GroupBy.Milestone;
}

/**
* Checks if the conditions to display the milestone anomalies warning card is met.
*/
private isDisplayMilestoneAnomalies(): boolean {
return this.showMilestoneAnomalies && this.milestoneService.hasMilestoneAnomalies() && this.isGroupByMilestone();
}
}
1 change: 1 addition & 0 deletions src/app/shared/filter-bar/filter-bar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field appearance="standard" class="filter-item">
<mat-label>Milestone</mat-label>
<mat-select
Expand Down
Loading
Loading