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

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