Skip to content

Commit 4cf6759

Browse files
authored
add project overview section
1 parent 7d95e61 commit 4cf6759

16 files changed

+301
-60
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface IDeveloperWithIssuesCount {
2+
login: string;
3+
issuesCount: number;
4+
issueType: 'assigned';
5+
}

src/interfaces/IProjectData.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface IProjectData {
44
inWorkIssues: IWrappedIssue[];
55
doneOrDeployIssues: IWrappedIssue[];
66
allPlannedIssues: IWrappedIssue[];
7+
toSolveIssues: IWrappedIssue[];
78
// raw:
89
backlogIssues: IWrappedIssue[];
910
committedIssues: IWrappedIssue[];

src/interfaces/IProjectStats.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
import { IDeveloperWithIssuesCount } from "./IDeveloperWithIssuesCount";
2+
13
export interface IProjectStats {
24
doneRate: number;
35
inWorkRate: number;
46
committedRate: number;
7+
8+
issuesDeveloperRatio: number;
9+
issuesDeveloperLeftRatio: number;
10+
11+
issuesDayRatio?: number;
12+
issuesDayLeftRatio?: number;
13+
14+
issuesDeveloperDayRatio?: number;
15+
issuesDeveloperDayLeftRatio?: number;
16+
17+
devWithMostAssignedIssues: IDeveloperWithIssuesCount;
18+
19+
developers: string[];
520
}

src/interfaces/IProjectWithData.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { IProjectData } from '../interfaces/IProjectData';
2+
import { IProjectWithConfig } from '../interfaces/IProjectWithConfig';
3+
4+
5+
export interface IProjectWithData {
6+
project: IProjectWithConfig;
7+
data: IProjectData;
8+
}

src/main.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,27 @@ import { IConfig } from './interfaces/IConfig';
1717
const TOKEN_NAME = 'REPO_GITHUB_PAT';
1818
const CONFIG_PATH = 'CONFIG_PATH';
1919

20-
const overwriteBoardIssue = async (issueContents: string, config: IConfig, projectKit: ProjectsOctoKit) => {
20+
const overwriteBoardIssue = async (
21+
issueContents: string,
22+
config: IConfig,
23+
projectKit: ProjectsOctoKit,
24+
) => {
2125
const { status } = await projectKit.updateBoardIssue(
2226
config.boardIssue,
2327
issueContents,
2428
);
2529

2630
if (status !== 200) {
27-
throw new Error(
28-
`Failed to update the issue ${config.boardIssue}`,
29-
);
31+
throw new Error(`Failed to update the issue ${config.boardIssue}`);
3032
}
3133

3234
console.log(`Successfully updated the board issue ${config.boardIssue}`);
33-
}
35+
};
3436

3537
const getRegex = (projectId?: number) => {
3638
const regex = /<!--\s*codespaces-board:start\s*-->([\W\w]*)<!--\s*codespaces-board:end\s*-->/gim;
3739
return regex;
38-
}
40+
};
3941

4042
const wrapIssueText = (text: string, projectId?: number) => {
4143
return [
@@ -45,15 +47,15 @@ const wrapIssueText = (text: string, projectId?: number) => {
4547
text,
4648
`<!-- codespaces-board:end -->`,
4749
].join('\n');
48-
}
50+
};
4951

50-
const updateBoardIssue = async (issueContents: string, config: IConfig, projectKit: ProjectsOctoKit) => {
52+
const updateBoardIssue = async (
53+
issueContents: string,
54+
config: IConfig,
55+
projectKit: ProjectsOctoKit,
56+
) => {
5157
if (!config.isReplaceProjectMarkers) {
52-
return await overwriteBoardIssue(
53-
issueContents,
54-
config,
55-
projectKit,
56-
);
58+
return await overwriteBoardIssue(issueContents, config, projectKit);
5759
}
5860

5961
const issue = await projectKit.getBoardIssue(
@@ -64,19 +66,22 @@ const updateBoardIssue = async (issueContents: string, config: IConfig, projectK
6466
const { body } = issue;
6567
const newBody = body.replace(getRegex(), wrapIssueText(issueContents));
6668

67-
await overwriteBoardIssue(
68-
newBody,
69-
config,
70-
projectKit,
71-
);
72-
}
69+
await overwriteBoardIssue(newBody, config, projectKit);
70+
};
7371

74-
const processConfigRecord = async (config: IConfig, projectKit: ProjectsOctoKit) => {
72+
const processConfigRecord = async (
73+
config: IConfig,
74+
projectKit: ProjectsOctoKit,
75+
) => {
7576
console.log('Processing config: \n', JSON.stringify(config, null, 2));
7677

7778
const validationErrors = validateConfig(config);
7879
if (validationErrors.length) {
79-
console.error(`\n\nNot valid config for the issue ${config.boardIssue}, skipping.. \n`, validationErrors, '\n\n');
80+
console.error(
81+
`\n\nNot valid config for the issue ${config.boardIssue}, skipping.. \n`,
82+
validationErrors,
83+
'\n\n',
84+
);
8085
return;
8186
}
8287

@@ -90,15 +95,14 @@ const processConfigRecord = async (config: IConfig, projectKit: ProjectsOctoKit)
9095
const data = await getProjectData(projectKit, repo, project);
9196
return {
9297
project,
93-
data
98+
data,
9499
};
95100
}),
96101
);
97102

98-
const result =
99-
projectsWithData.map(({ project, data }) => {
100-
return renderProject(data, project);
101-
});
103+
const result = projectsWithData.map(({ project, data }) => {
104+
return renderProject(data, project, config);
105+
});
102106

103107
const issueBody = result.join('\n') + '\n';
104108
let header;
@@ -115,12 +119,12 @@ const processConfigRecord = async (config: IConfig, projectKit: ProjectsOctoKit)
115119
header,
116120
renderOverview(config, projectsWithData),
117121
issueBody,
118-
footer
122+
footer,
119123
].join('\n');
120124

121125
await updateBoardIssue(issueContents, config, projectKit);
122126
}
123-
}
127+
};
124128

125129
async function run(): Promise<void> {
126130
try {

src/utils/arrayUnique.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const arrayUnique = <T>(array: T[]): T[] => {
2+
const set = new Set<T>(array);
3+
4+
return [...set];
5+
};

src/utils/flatternArray.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const flattenArray = <T>(
2+
input: Array<any>,
3+
output: Array<T> = [],
4+
): Array<T> => {
5+
for (const value of input) {
6+
Array.isArray(value) ? flattenArray(value, output) : output.push(value);
7+
}
8+
return output;
9+
};

src/utils/getProjectData.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,14 @@ export const getProjectData = async (
5858
const inWorkIssues = [...progressIssues, ...inReviewIssues];
5959
const doneOrDeployIssues = [...waitingToDeployIssues, ...doneIssues];
6060
const allPlannedIssues = [...blockedIssues, ...committedIssues, ...inWorkIssues, ...doneOrDeployIssues];
61+
const toSolveIssues = [...inWorkIssues, ...blockedIssues, ...committedIssues];
6162

6263
return {
6364
// combined
6465
inWorkIssues,
6566
doneOrDeployIssues,
6667
allPlannedIssues,
68+
toSolveIssues,
6769
// plain
6870
backlogIssues,
6971
committedIssues,

src/utils/getProjectStats.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,123 @@
1+
import { IConfig } from '../interfaces/IConfig';
12
import { IProjectData } from '../interfaces/IProjectData';
23
import { IProjectStats } from '../interfaces/IProjectStats';
4+
import { IWrappedIssue } from '../interfaces/IWrappedIssue';
5+
import { TRepoIssue } from '../interfaces/TRepoIssue';
6+
import { getWorkDays } from '../views/renderDaysLeft';
7+
import { arrayUnique } from './arrayUnique';
8+
import { flattenArray } from './flatternArray';
9+
import { IDeveloperWithIssuesCount } from '../interfaces/IDeveloperWithIssuesCount';
10+
import { notEmpty } from './notEmpty';
11+
import { pluck } from './pluck';
312

4-
export const getProjectStats = (data: IProjectData): IProjectStats => {
13+
const getDevelopers = (issues: IWrappedIssue[]): string[] => {
14+
const developersWithDuplicates = issues
15+
.map(({ issue }) => {
16+
return issue.assignees.map(pluck('login'));
17+
})
18+
.filter(notEmpty);
19+
20+
const developers = arrayUnique(flattenArray(developersWithDuplicates));
21+
22+
return developers as string[];
23+
};
24+
25+
const asigneesToDevelopers = (assignees: TRepoIssue['assignees']) => {
26+
return assignees.map((as) => {
27+
return as.login;
28+
});
29+
}
30+
31+
const countAssignedIssues = (developer: string, issues: IWrappedIssue[]) => {
32+
const result = issues.reduce((current, { issue }) => {
33+
const { assignees } = issue;
34+
35+
if (!assignees || !assignees.length) {
36+
return current;
37+
}
38+
39+
const developers = asigneesToDevelopers(assignees);
40+
return (developers.includes(developer))
41+
? current + 1
42+
: current;
43+
}, 0);
44+
45+
return result;
46+
}
47+
48+
const getBusiestDeveloper = (issues: IWrappedIssue[]): IDeveloperWithIssuesCount => {
49+
const developers = getDevelopers(issues)
50+
.map<IDeveloperWithIssuesCount>((developer) => {
51+
return {
52+
login: developer,
53+
issuesCount: countAssignedIssues(developer, issues),
54+
issueType: 'assigned',
55+
}
56+
})
57+
.sort((dev1, dev2) => {
58+
return dev2.issuesCount - dev1.issuesCount;
59+
});
60+
61+
return developers[0];
62+
};
63+
64+
export const getProjectStats = (data: IProjectData, config: IConfig): IProjectStats => {
565
const {
666
// combined
767
inWorkIssues,
868
doneOrDeployIssues,
969
allPlannedIssues,
70+
toSolveIssues,
1071
// plain
1172
committedIssues,
1273
} = data;
1374

75+
const daysLeft = getWorkDays(config);
76+
1477
const doneRate = doneOrDeployIssues.length / allPlannedIssues.length;
1578
const inWorkRate = inWorkIssues.length / allPlannedIssues.length;
1679
const committedRate = committedIssues.length / allPlannedIssues.length;
1780

81+
const developers = getDevelopers(allPlannedIssues);
82+
83+
const issuesDeveloperLeftRatio = toSolveIssues.length / developers.length;
84+
const issuesDeveloperRatio = allPlannedIssues.length / developers.length;
85+
86+
const issuesDayLeftRatio = (daysLeft)
87+
? toSolveIssues.length / Math.max(daysLeft.businessDaysLeft, 1)
88+
: undefined;
89+
90+
const issuesDayRatio = (daysLeft)
91+
? allPlannedIssues.length / daysLeft.totalBusinessDays
92+
: undefined;
93+
94+
const issuesDeveloperDayLeftRatio = (issuesDayLeftRatio)
95+
? issuesDayLeftRatio / developers.length
96+
: undefined;
97+
98+
const issuesDeveloperDayRatio = (issuesDayRatio)
99+
? issuesDayRatio / developers.length
100+
: undefined;
101+
18102
return {
19103
doneRate,
20104
inWorkRate,
21105
committedRate,
106+
// number of developers on the board
107+
developers,
108+
// inital issues per developer load
109+
issuesDeveloperRatio,
110+
// how many issues per developer left to solve
111+
issuesDeveloperLeftRatio,
112+
// inital issues per day load
113+
issuesDayRatio,
114+
// how many issues per day left to solve
115+
issuesDayLeftRatio,
116+
// inital per day per developer ratio
117+
issuesDeveloperDayRatio,
118+
// how many issues per day per developer left to solve
119+
issuesDeveloperDayLeftRatio,
120+
// developer with most assigned issues
121+
devWithMostAssignedIssues: getBusiestDeveloper(toSolveIssues),
22122
};
23123
};

src/utils/pluck.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
export const pluck = (propName: string) => {
3+
return (obj: any) => {
4+
return obj[propName];
5+
};
6+
};

0 commit comments

Comments
 (0)