This commit is contained in:
bakhirev 2024-10-25 00:08:51 +03:00
parent 6362f71a80
commit 57008ac20e
23 changed files with 207 additions and 56 deletions
build/static
src/ts
components/TimeZoneMap
components
helpers
styles
helpers
DataGrip
Recommendations/components
formatter.ts
pages/Team
components/Country
styles
store
translations

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
import React from 'react';
import { getClassNameForTimeZone } from '../helpers';
import { getPositionForTimeZone, getColorForTimeZone } from '../helpers';
import style from '../styles/index.module.scss';
@ -13,11 +13,12 @@ function Point({
timezone,
authors,
}: PointProps): React.ReactElement | null {
const className = getClassNameForTimeZone(timezone);
const position = getPositionForTimeZone(timezone);
const color = getColorForTimeZone(authors);
return (
<div
title={authors.join(', ')}
className={`${style.time_zone_map_point} ${className}`}
className={`${style.time_zone_map_point} ${position} ${color}`}
>
{authors.length}
</div>

View file

@ -1,3 +1,5 @@
import dataGripStore from 'ts/store/DataGrip';
import style from '../styles/index.module.scss';
const REF_TIMEZONE_CLASS = {
@ -46,10 +48,23 @@ export function getGroupsByTimeZone(authors: any[]) {
}, {});
}
export function getClassNameForTimeZone(timezone?: string) {
export function getPositionForTimeZone(timezone?: string) {
const suffix = (timezone || '')
.replace('+', 'p')
.replace('-', 'm')
.replace(':', '');
return REF_TIMEZONE_CLASS[suffix] || style.time_zone_map_point_hide;
}
export function getColorForTimeZone(authors: string[]) {
let isDismissed = false;
for (let i = 0, l = authors.length; i < l; i++) {
const item = dataGripStore.dataGrip.author.statisticByName[authors[i]];
if (item?.isStaff) continue;
if (!item?.isDismissed) return style.time_zone_map_point_active;
if (item?.isDismissed) isDismissed = true;
}
return isDismissed
? style.time_zone_map_point_dismissed
: '';
}

View file

@ -42,7 +42,7 @@
color: var(--color-white);
border-radius: var(--border-radius-l);
background-color: var(--color-second);
background-color: var(--color-black);
}
}
@ -82,4 +82,6 @@
&_m1100 { top: 67%; left: 0; }
&_m1200 { top: 62%; left: 97%; }
&_hide { display: none }
&_active { background-color: var(--color-first) }
&_dismissed { background-color: var(--color-second) }
}

View file

@ -2,7 +2,7 @@ import ICommit from 'ts/interfaces/Commit';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { ONE_DAY } from 'ts/helpers/formatter';
import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
import { createIncrement, increment } from 'ts/helpers/Math';
import userSettings from 'ts/store/UserSettings';
@ -24,21 +24,21 @@ export default class DataGripByAuthor {
this.statisticByName = {};
}
addCommit(commit: ICommit) {
addCommit(commit: ICommit, totalCommits: number) {
const statistic = this.commits.get(commit.author);
if (statistic) {
this.#updateCommitByAuthor(statistic, commit);
this.#updateCommitByAuthor(statistic, commit, totalCommits);
} else {
this.#addCommitByAuthor(commit);
}
this.#setMoneyByMonth(commit);
}
#updateCommitByAuthor(statistic: any, commit: ICommit) {
#updateCommitByAuthor(statistic: any, commit: ICommit, totalCommits: number) {
statistic.commits += 1;
statistic.lastCommit = commit;
statistic.device = statistic.device || commit.device;
statistic.days[commit.timestamp] = true;
statistic.days.set(commit.timestamp, true);
statistic.tasks[commit.task] = commit.added + commit.changes + commit.removed
+ (statistic.tasks[commit.task] ? statistic.tasks[commit.task] : 0);
increment(statistic.types, commit.type);
@ -55,7 +55,9 @@ export default class DataGripByAuthor {
debugger;
}
statistic.commitsByHour[commit.hours] += 1;
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
if (totalCommits < 50000) {
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
}
if (commit.company && statistic.lastCompany !== commit.company) {
statistic.lastCompany = commit.company;
@ -80,7 +82,7 @@ export default class DataGripByAuthor {
commits: 1,
firstCommit: commit,
lastCommit: commit,
days: createHashMap(commit.timestamp),
days: new Map([[commit.timestamp, true]]),
tasks: { [commit.task]: commit.added + commit.changes + commit.removed },
types: createIncrement(commit.type),
scopes: createIncrement(commit.scope),
@ -171,7 +173,7 @@ export default class DataGripByAuthor {
const from = dot.firstCommit.milliseconds;
const to = dot.lastCommit.milliseconds;
const workDays = Object.keys(dot.days).length;
const workDays = dot.days.size;
const allDaysInProject = Math.ceil((to - from) / ONE_DAY);
const lazyDays = Math.floor((allDaysInProject * WORK_AND_HOLIDAYS) - workDays) + 1;
@ -242,8 +244,16 @@ export default class DataGripByAuthor {
...this.employment.staff,
];
this.updateSort();
}
updateSort() {
const position = new Map();
this.list.forEach((name: string, index: number) => {
position.set(name, index);
});
this.statistic.sort((a: any, b: any) => (
this.list.indexOf(a.author) - this.list.indexOf(b.author)
position.get(a.author) - position.get(b.author)
));
}

View file

@ -1,19 +1,19 @@
import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import userSettings from 'ts/store/UserSettings';
import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
import { createIncrement, increment } from 'ts/helpers/Math';
interface IStatByAuthor {
commits: number; // number of commits by author in this scope
days: IHashMap<boolean>; // commit timestamp
days: HashMap<boolean>; // commit timestamp
types: IHashMap<number>; // commit type by author in this scope (fix, feat)
}
interface IStatByScope {
scope: string; // scope name
commits: number; // number of commits in this scope
days: IHashMap<boolean>; // commit timestamp
tasks: IHashMap<boolean>; // task name in this scope (JIRA-123)
days: HashMap<boolean>; // commit timestamp
tasks: HashMap<boolean>; // task name in this scope (JIRA-123)
types: IHashMap<number>; // commit type in this scope (fix, feat)
authors: IHashMap<IStatByAuthor>; // stat by author for this scope
}
@ -42,14 +42,14 @@ export default class DataGripByScope {
#updateCommitByScope(commit: ICommit) {
const statistic = this.commits[commit.scope] as IStatByScope;
statistic.commits += 1;
statistic.days[commit.timestamp] = true;
statistic.tasks[commit.task] = true;
statistic.days.set(commit.timestamp, true);
statistic.tasks.set(commit.task, true);
increment(statistic.types, commit.type);
const author = statistic.authors[commit.author];
if (author) {
author.commits += 1;
author.days[commit.timestamp] = true;
author.days.set(commit.timestamp, true);
increment(author.types, commit.type);
} else {
statistic.authors[commit.author] = this.#getDefaultAuthorForScope(commit);
@ -60,8 +60,8 @@ export default class DataGripByScope {
this.commits[commit.scope] = {
scope: commit.scope,
commits: 1,
days: createHashMap(commit.timestamp),
tasks: createHashMap(commit.task),
days: new Map([[commit.timestamp, true]]),
tasks: new Map([[commit.task, true]]),
types: createIncrement(commit.type),
authors: createIncrement(commit.author, this.#getDefaultAuthorForScope(commit)),
};
@ -70,7 +70,7 @@ export default class DataGripByScope {
#getDefaultAuthorForScope(commit: ICommit): IStatByAuthor {
return {
commits: 1,
days: { [commit.timestamp]: true },
days: new Map([[commit.timestamp, true]]),
types: { [commit.type]: 1 },
};
}
@ -84,18 +84,20 @@ export default class DataGripByScope {
let cost = 0;
for (let name in dot.authors) {
const user = dot.authors[name];
const days: number = Object.keys(user.days).length;
const days: number = user.days.size;
// TODO: need middle salary in month;
salaryCache[name] = salaryCache[name] || userSettings.getCurrentSalaryInDay(name);
cost += days * salaryCache[name];
dot.authors[name] = { ...user, days };
}
dot.tasks.delete('');
return {
...dot,
days: Object.keys(dot.days).length,
days: dot.days.size,
cost,
tasks: Object.keys(dot.tasks).filter(t => t),
tasks: dot.tasks.size,
};
});

View file

@ -27,8 +27,8 @@ export default class DataGripByType {
#updateCommitByType(commit: ICommit) {
const statistic = this.commits[commit.type];
statistic.commits += 1;
statistic.days[commit.timestamp] = true;
statistic.tasks[commit.task] = true;
statistic.days.set(commit.timestamp, true);
statistic.tasks.set(commit.task, true);
increment(statistic.commitsByAuthors, commit.author);
if (!statistic.daysByAuthors[commit.author]) statistic.daysByAuthors[commit.author] = {};
@ -39,8 +39,8 @@ export default class DataGripByType {
this.commits[commit.type] = {
type: commit.type,
commits: 1,
days: createIncrement(commit.timestamp, true),
tasks: createIncrement(commit.task, true),
days: new Map([[commit.timestamp, true]]),
tasks: new Map([[commit.task, true]]),
commitsByAuthors: createIncrement(commit.author, true),
daysByAuthors: {
[commit.author]: createIncrement(commit.timestamp, true),
@ -56,8 +56,8 @@ export default class DataGripByType {
.filter((dot: any) => dot.commits > 5 || isCorrectType[dot?.type || ''])
.map((dot: any) => ({
...dot,
tasks: Object.keys(dot.tasks).length,
days: Object.keys(dot.days).length,
tasks: dot.tasks.size,
days: dot.days.size,
daysByAuthorsTotal: Object.values(dot.daysByAuthors)
.reduce((t: number, v: any) => (t + Object.keys(v).length), 0),
}))

View file

@ -66,13 +66,13 @@ class DataGrip {
this.scoring.clear();
}
addCommit(commit: ICommit | ISystemCommit) {
addCommit(commit: ICommit | ISystemCommit, totalCommits: number) {
if (commit.author === 'GitHub') return;
this.pr.addCommit(commit); // @ts-ignore
this.release.addCommit(commit); // @ts-ignore
if (!commit.commitType) {
this.firstLastCommit.update(commit.milliseconds, commit);
this.author.addCommit(commit);
this.author.addCommit(commit, totalCommits);
this.scope.addCommit(commit);
this.type.addCommit(commit);
this.timestamp.addCommit(commit);

View file

@ -28,7 +28,9 @@ export default class RecommendationsTeamByType {
getBusFactor(dataGrip: any) {
if (dataGrip.author.list.length < 2) return null;
if (dataGrip.type.statistic.length > 200) return null; // for performance
// TODO: bad performance
const oneMaintainer = dataGrip.type.statistic.filter((statistic: any) => {
const limit = statistic.commits * 0.8;
return dataGrip.author.list.some((name: string) => statistic.commitsByAuthors[name] >= limit);

View file

@ -29,9 +29,17 @@ const TIMESTAMP = [
ONE_DAY * 3,
];
export function getDayName(index:number, weekday: 'long' | 'short') {
// for performance
const dayNameCache = new Map();
export function getDayName(index:number, weekday: 'long' | 'short') { // @ts-ignore
const code = window?.localization?.language || 'ru';
const response = dayNameCache.get(`${code}${index}${weekday}`);
if (response) return response;
const date = new Date(TIMESTAMP[index]);
return date.toLocaleString(getLangPrefix(), { weekday: weekday || 'long' });
const dayName = date.toLocaleString(getLangPrefix(), { weekday: weekday || 'long' });
dayNameCache.set(`${code}${index}${weekday}`, dayName);
return dayName;
}
export function getDateByTimestamp(timestamp: string) {

View file

@ -0,0 +1,77 @@
import React, { useMemo, useState } from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import SelectWithButtons from 'ts/components/UiKit/components/SelectWithButtons';
import UiKitCheckbox from 'ts/components/UiKit/components/Checkbox';
import TimeZoneMap from 'ts/components/TimeZoneMap';
import PageWrapper from 'ts/components/Page/Box';
import Title from 'ts/components/Title';
import { t } from 'ts/helpers/Localization';
import style from '../../../styles/country.module.scss';
function getOptions(companies: any[]) {
const options = companies.map((item: any) => ({ id: item.company, title: item.company }));
return [
{ id: '', title: t('page.common.filter.allUsers') },
{ id: Math.random(), title: 'Unknown' },
...options,
];
}
const CustomMap = observer(() => {
const companies = dataGripStore.dataGrip.company.statistic;
const companyOptions = useMemo(() => getOptions(companies), companies);
const [company, setCompany] = useState<string>('');
const [isStaff, setIsStaff] = useState<boolean>(true);
const [isActive, setIsActive] = useState<boolean>(true);
const [isDismissed, setIsDismissed] = useState<boolean>(true);
const authors = dataGripStore.dataGrip.author.statistic
.filter((author: any) => {
if (company && author.lastCompany !== company) return false;
if (!isStaff && author.isStaff) return false;
if (!isActive && !author.isDismissed && !author.isStaff) return false;
if (!isDismissed && author.isDismissed && !author.isStaff) return false;
return true;
});
return (
<PageWrapper>
<Title title="page.team.country.byTimezone"/>
<TimeZoneMap authors={authors}/>
<div className={style.team_country_filter}>
<UiKitCheckbox
title="page.team.country.filters.active"
className={style.team_country_filter_checkbox}
value={isActive}
onChange={() => setIsActive(!isActive)}
/>
<UiKitCheckbox
title="page.team.country.filters.dismissed"
className={style.team_country_filter_checkbox}
value={isDismissed}
onChange={() => setIsDismissed(!isDismissed)}
/>
<UiKitCheckbox
title="page.team.country.filters.staff"
className={style.team_country_filter_checkbox}
value={isStaff}
onChange={() => setIsStaff(!isStaff)}
/>
<SelectWithButtons
title="page.team.tree.filters.author"
className={style.team_country_filter_select}
value={company}
options={companyOptions}
onChange={(id: string) => setCompany(id)}
/>
</div>
</PageWrapper>
);
});
export default CustomMap;

View file

@ -13,10 +13,9 @@ import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title';
import Countries from './components/Countries';
import CountryCharts from './components/Charts';
import TimeZoneMap from 'ts/components/TimeZoneMap';
import PageWrapper from 'ts/components/Page/Box';
import fullScreen from 'ts/store/FullScreen';
import CustomMap from './components/Map';
import Travel from './components/Travel';
const Country = observer(({
@ -36,15 +35,8 @@ const Country = observer(({
return (
<>
{!fullScreen.isOpen && (
<>
<PageWrapper>
<Title title="page.team.country.byTimezone"/>
<TimeZoneMap authors={authors}/>
</PageWrapper>
<CountryCharts/>
</>
)}
{!fullScreen.isOpen && <CustomMap />}
{!fullScreen.isOpen && <CountryCharts />}
{canShowByCountries ? (
<>

View file

@ -0,0 +1,16 @@
@import 'src/styles/variables';
.team_country_filter {
margin-top: var(--space-m);
&_checkbox,
&_select {
display: inline-block;
margin-right: var(--space-xxl);
vertical-align: middle;
}
&_select {
min-width: 350px;
}
}

View file

@ -65,8 +65,10 @@ class DataGripStore {
processingCommitGrouping(commits: (ICommit | ISystemCommit)[]) {
commits.sort((a, b) => a.milliseconds - b.milliseconds);
const totalCommits = commits.length;
commits.forEach((commit: ICommit | ISystemCommit) => {
dataGrip.addCommit(commit);
dataGrip.addCommit(commit, totalCommits);
});
setTimeout(() => this.processingFileGrouping(commits), PROCESSING_DELAY);
@ -128,7 +130,7 @@ class DataGripStore {
? depersonalized.getCommit(commit)
: commit;
dataGrip.addCommit(localCommit);
dataGrip.addCommit(localCommit, 0);
fileGrip.addCommit(localCommit);
});

View file

@ -130,6 +130,9 @@ export default `
§ page.team.company.active.yes: active
§ page.team.company.active.no: contract has expired
§ page.team.country.byTimezone: By the time of the last commit
§ page.team.country.filters.active: Works
§ page.team.country.filters.dismissed: Dismissed
§ page.team.country.filters.staff: Staff
§ page.team.country.pieByDomain.title: By email, timezone and language
§ page.team.country.pieByTimezone.title: By timezone
§ page.team.country.chart.item: employments

View file

@ -130,6 +130,9 @@ export default `
§ page.team.company.active.yes: active
§ page.team.company.active.no: contract has expired
§ page.team.country.byTimezone: By the time of the last commit
§ page.team.country.filters.active: Works
§ page.team.country.filters.dismissed: Dismissed
§ page.team.country.filters.staff: Staff
§ page.team.country.pieByDomain.title: By email, timezone and language
§ page.team.country.pieByTimezone.title: By timezone
§ page.team.country.chart.item: employments

View file

@ -130,6 +130,9 @@ export default `
§ page.team.company.active.yes: active
§ page.team.company.active.no: contract has expired
§ page.team.country.byTimezone: By the time of the last commit
§ page.team.country.filters.active: Works
§ page.team.country.filters.dismissed: Dismissed
§ page.team.country.filters.staff: Staff
§ page.team.country.pieByDomain.title: By email, timezone and language
§ page.team.country.pieByTimezone.title: By timezone
§ page.team.country.chart.item: employments

View file

@ -130,6 +130,9 @@ export default `
§ page.team.company.active.yes: active
§ page.team.company.active.no: contract has expired
§ page.team.country.byTimezone: By the time of the last commit
§ page.team.country.filters.active: Works
§ page.team.country.filters.dismissed: Dismissed
§ page.team.country.filters.staff: Staff
§ page.team.country.pieByDomain.title: By email, timezone and language
§ page.team.country.pieByTimezone.title: By timezone
§ page.team.country.chart.item: employments

View file

@ -130,6 +130,9 @@ export default `
§ page.team.company.active.yes: active
§ page.team.company.active.no: contract has expired
§ page.team.country.byTimezone: By the time of the last commit
§ page.team.country.filters.active: Works
§ page.team.country.filters.dismissed: Dismissed
§ page.team.country.filters.staff: Staff
§ page.team.country.pieByDomain.title: By email, timezone and language
§ page.team.country.pieByTimezone.title: By timezone
§ page.team.country.chart.item: employments

View file

@ -130,6 +130,9 @@ export default `
§ page.team.company.active.yes: active
§ page.team.company.active.no: contract has expired
§ page.team.country.byTimezone: By the time of the last commit
§ page.team.country.filters.active: Works
§ page.team.country.filters.dismissed: Dismissed
§ page.team.country.filters.staff: Staff
§ page.team.country.pieByDomain.title: By email, timezone and language
§ page.team.country.pieByTimezone.title: By timezone
§ page.team.country.chart.item: employments

View file

@ -130,6 +130,9 @@ export default `
§ page.team.company.active.yes: активна
§ page.team.company.active.no: контракт истёк
§ page.team.country.byTimezone: По времени последнего коммита
§ page.team.country.filters.active: Работают
§ page.team.country.filters.dismissed: Уволенные
§ page.team.country.filters.staff: Помощники
§ page.team.country.pieByDomain.title: По почте, времени и языку
§ page.team.country.pieByTimezone.title: По времени
§ page.team.country.chart.item: сотрудников

View file

@ -125,6 +125,9 @@ export default `
§ page.team.company.active.yes: active
§ page.team.company.active.no: contract has expired
§ page.team.country.byTimezone: By the time of the last commit
§ page.team.country.filters.active: Works
§ page.team.country.filters.dismissed: Dismissed
§ page.team.country.filters.staff: Staff
§ page.team.country.pieByDomain.title: By email, timezone and language
§ page.team.country.pieByTimezone.title: By timezone
§ page.team.country.chart.item: employments