This commit is contained in:
bakhirev 2024-10-17 17:03:12 +03:00
parent 1acd5a0c27
commit de4ef1409c
52 changed files with 2887 additions and 42 deletions

BIN
build/assets/map/2x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

BIN
build/assets/map/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke="#84858D" d="M12 7V3H2v18h20V7zM6 19H4v-2h2zm0-4H4v-2h2zm0-4H4V9h2zm0-4H4V5h2zm4 12H8v-2h2zm0-4H8v-2h2zm0-4H8V9h2zm0-4H8V5h2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8zm-2-8h-2v2h2zm0 4h-2v2h2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 315 B

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#84858D" d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7M7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9"></path>
<circle fill="#84858D" cx="12" cy="9" r="2.5"></circle>
</svg>

After

Width:  |  Height:  |  Size: 347 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke="#84858D" d="M4 7v2c0 .55-.45 1-1 1s-1 .45-1 1v2c0 .55.45 1 1 1s1 .45 1 1v2c0 1.66 1.34 3 3 3h2c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1-.45-1-1v-2c0-1.3-.84-2.42-2-2.83v-.34C5.16 11.42 6 10.3 6 9V7c0-.55.45-1 1-1h2c.55 0 1-.45 1-1s-.45-1-1-1H7C5.34 4 4 5.34 4 7m17 3c-.55 0-1-.45-1-1V7c0-1.66-1.34-3-3-3h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c.55 0 1 .45 1 1v2c0 1.3.84 2.42 2 2.83v.34c-1.16.41-2 1.52-2 2.83v2c0 .55-.45 1-1 1h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c1.66 0 3-1.34 3-3v-2c0-.55.45-1 1-1s1-.45 1-1v-2c0-.55-.45-1-1-1"></path>
</svg>

After

Width:  |  Height:  |  Size: 643 B

File diff suppressed because one or more lines are too long

View file

@ -1,18 +1,16 @@
module.exports = {
webpack: (config) => {
const oneOfs = config.module.rules.find((rule) => !!rule.oneOf).oneOf;
for (const oneOf of oneOfs) {
oneOf?.use?.forEach((someUse) => {
if (!someUse?.options?.modules?.mode) return;
// someUse.options.modules.localIdentName = '[local]_';
someUse.options.modules.getLocalIdent = (context, localIdentName, localName, options) => {
// someUse.options.modules.localIdentName = '[local]';
someUse.options.modules.getLocalIdent = (context, localIdentName, localName) => {
return localName;
}
};
});
}
return config;
}
},
};

BIN
public/assets/map/2x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

BIN
public/assets/map/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke="#84858D" d="M12 7V3H2v18h20V7zM6 19H4v-2h2zm0-4H4v-2h2zm0-4H4V9h2zm0-4H4V5h2zm4 12H8v-2h2zm0-4H8v-2h2zm0-4H8V9h2zm0-4H8V5h2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8zm-2-8h-2v2h2zm0 4h-2v2h2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 315 B

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#84858D" d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7M7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9"></path>
<circle fill="#84858D" cx="12" cy="9" r="2.5"></circle>
</svg>

After

Width:  |  Height:  |  Size: 347 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke="#84858D" d="M4 7v2c0 .55-.45 1-1 1s-1 .45-1 1v2c0 .55.45 1 1 1s1 .45 1 1v2c0 1.66 1.34 3 3 3h2c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1-.45-1-1v-2c0-1.3-.84-2.42-2-2.83v-.34C5.16 11.42 6 10.3 6 9V7c0-.55.45-1 1-1h2c.55 0 1-.45 1-1s-.45-1-1-1H7C5.34 4 4 5.34 4 7m17 3c-.55 0-1-.45-1-1V7c0-1.66-1.34-3-3-3h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c.55 0 1 .45 1 1v2c0 1.3.84 2.42 2 2.83v.34c-1.16.41-2 1.52-2 2.83v2c0 .55-.45 1-1 1h-2c-.55 0-1 .45-1 1s.45 1 1 1h2c1.66 0 3-1.34 3-3v-2c0-.55.45-1 1-1s1-.45 1-1v-2c0-.55-.45-1-1-1"></path>
</svg>

After

Width:  |  Height:  |  Size: 643 B

View file

@ -0,0 +1,32 @@
import React from 'react';
import { getClassNameForTimeZone } from '../helpers';
import style from '../styles/index.module.scss';
interface PointProps {
timezone: string;
authors: string[]
}
function Point({
timezone,
authors,
}: PointProps): React.ReactElement | null {
const className = getClassNameForTimeZone(timezone);
return (
<div
title={authors.join(', ')}
className={`${style.time_zone_map_point} ${className}`}
>
{authors.length}
</div>
);
}
Point.defaultProps = {
timezone: '',
authors: [],
};
export default Point;

View file

@ -0,0 +1,16 @@
export function getGroupsByTimeZone(authors: any[]) {
return authors.reduce((acc: any, author: any) => {
const key = author.lastCommit.timezone;
if (!acc[key]) acc[key] = [];
acc[key].push(author.author);
return acc;
}, {});
}
export function getClassNameForTimeZone(timezone?: string) {
const suffix = (timezone || '')
.replace('+', 'p')
.replace('-', 'm')
.replace(':', '');
return `time_zone_map_point_${suffix || 'hide'}`;
}

View file

@ -0,0 +1,39 @@
import React from 'react';
import { getGroupsByTimeZone } from './helpers';
import Point from './components/Point';
import style from './styles/index.module.scss';
interface TimeZoneMapProps {
authors: any[]
}
function TimeZoneMap({
authors = [],
}: TimeZoneMapProps): React.ReactElement | null {
const groups = getGroupsByTimeZone(authors);
const points = Object.entries(groups).map((item: any) => (
<Point
key={item[0]}
timezone={item[0]}
authors={item[1]}
/>
));
return (
<div className={style.time_zone_map}>
<img
src="./assets/map/2x1.png"
className={style.time_zone_map_gap}
/>
<div
style={{ backgroundImage: 'url(./assets/map/map.png)' }}
className={style.time_zone_map_points}
>
{points}
</div>
</div>
);
}
export default TimeZoneMap;

View file

@ -0,0 +1,84 @@
@import 'src/styles/variables';
@import 'src/ts/components/Description/index.module';
.time_zone_map {
position: relative;
display: block;
margin: 0 auto;
text-align: center;
&_gap {
display: block;
width: 100%;
}
&_points {
position: absolute;
top: 0;
left: 0;
display: inline-block;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: 100% 100%;
background-position: center center;
}
&_point {
font-size: var(--font-xxs);
position: absolute;
top: 0;
left: 0;
display: block;
width: var(--space-xl);
height: var(--space-xl);
text-align: center;
line-height: 20px;
box-shadow: 0 0 5px var(--color-white), 0 0 5px var(--color-white), 0 0 15px var(--color-white);
color: var(--color-white);
border-radius: var(--border-radius-l);
background-color: var(--color-second);
}
}
.time_zone_map_point {
&_p1300 { top: 72%; left: 97.5%; }
&_p1200 { top: 87%; left: 94%; }
&_p1100 { top: 81%; left: 88%; } // { top: 26%; left: 88%; }
&_p1000 { top: 83.5%; left: 86.5%; }
&_p1030 { top: 82%; left: 84%; }
&_p0930 { top: 82%; left: 84%; }
&_p0900 { top: 42%; left: 83%; }
&_p0800 { top: 47%; left: 78%; }
&_p0700 { top: 29%; left: 69%; }
&_p0630 { top: 53%; left: 73%; }
&_p0600 { top: 29%; left: 66.5%; }
&_p0545 { top: 47%; left: 69%; }
&_p0430 { top: 43%; left: 65%; }
&_p0530 { top: 49.5%; left: 67%; }
&_p0500 { top: 30%; left: 64%; }
&_p0400 { top: 31%; left: 60%; }
&_p0330 { top: 44%; left: 61%; }
&_p0300 { top: 29%; left: 57%; }
&_p0200 { top: 33.5%; left: 54%; }
&_p0100 { top: 35%; left: 48%; }
&_p0000 { top: 32%; left: 45%; }
&_m0100 { top: 41%; left: 39%; }
&_m0200 { top: 75%; left: 33.5%; } // { top: 18%; left: 37%; }
&_m0300 { top: 81%; left: 30%; }
&_m0330 { top: 34%; left: 30.5%; }
&_m0400 { top: 68%; left: 28.5%; }
&_m0500 { top: 42%; left: 25%; }
&_m0600 { top: 45%; left: 19%; }
&_m0700 { top: 42%; left: 16%; }
&_m0800 { top: 39%; left: 12%; }
&_m0900 { top: 22%; left: 5%; }
&_m1000 { top: 74%; left: 4%; }
&_m1100 { top: 67%; left: 0; }
&_m1200 { top: 62%; left: 97%; }
}

View file

@ -2,8 +2,6 @@ import ICommit from 'ts/interfaces/Commit';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { ONE_DAY } from 'ts/helpers/formatter';
import getCountryByTimeZone from 'ts/helpers/Parser/getCountryByTimeZone';
import getCountryBySymbol from 'ts/helpers/Parser/getCountryBySymbol';
import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
import userSettings from 'ts/store/UserSettings';
@ -63,9 +61,10 @@ export default class DataGripByAuthor {
statistic.lastCompany = commit.company;
statistic.company.push({ title: commit.company, from: commit.timestamp });
}
if (commit.country && statistic.lastCountry !== commit.country) {
if (commit.timezone && statistic.lastCountry !== commit.timezone) {
statistic.lastTimezone = commit.timezone;
statistic.lastCountry = commit.country;
statistic.country.add(commit.country);
statistic.country.push({ country: commit.country, timezone: commit.timezone, from: commit.milliseconds });
}
}
@ -76,11 +75,6 @@ export default class DataGripByAuthor {
const commitsByHour = new Array(24).fill(0);
commitsByHour[commit.hours] += 1;
const country = commit.country
|| getCountryBySymbol(commit.author)
|| getCountryBySymbol(commit.message)
|| getCountryByTimeZone(commit.timezone, commit.author);
this.commits.set(commit.author, {
author: commit.author,
commits: 1,
@ -95,8 +89,9 @@ export default class DataGripByAuthor {
? [{ title: commit.company, from: commit.milliseconds }]
: [],
lastCompany: commit.company,
country: new Set([country]),
lastCountry: country,
country: [{ country: commit.country, timezone: commit.timezone, from: commit.milliseconds }],
lastTimezone: commit.timezone,
lastCountry: commit.country,
device: commit.device,
commitsByDayAndHour,
commitsByHour,

View file

@ -0,0 +1,49 @@
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { increment } from 'ts/helpers/Math';
import { getVpnList, getTravels } from 'ts/helpers/Parser/getCountryDistance';
export default class DataGripByCountry {
countries: HashMap<any> = new Map();
vpn: IHashMap<any> = {};
devices: IHashMap<any> = {};
statistic: any = [];
clear() {
this.countries.clear();
this.vpn = {};
this.devices = {};
this.statistic = [];
}
#addAuthor(country: string, author: string) {
const statistic = this.countries.get(country);
if (statistic) {
statistic.employments.push(author);
} else {
this.countries.set(country, {
country,
employments: [author],
});
}
}
updateTotalInfo(dataGripByAuthor: any) {
dataGripByAuthor.statistic.forEach((author: any) => {
const vpnList = getVpnList(author.country);
author.country = getTravels(author.country, vpnList);
this.#addAuthor(author.lastCountry || 'unknown', author.author);
increment(this.devices, author.device || 'unknown');
Array.from(vpnList.keys()).forEach((country: string) => {
increment(this.vpn, country);
});
});
this.statistic = Array.from(this.countries.values())
.sort((a: any, b: any) => b?.employments?.length - a?.employments?.length);
this.countries.clear();
}
}

View file

@ -0,0 +1,23 @@
import ICommit from 'ts/interfaces/Commit';
import { IDirtyFile } from 'ts/interfaces/FileInfo';
export default class FileBuilderTasks {
static getProps(commit: ICommit) {
return {
tasks: new Set([commit.task]),
timestamp: new Set([commit.timestamp]),
totalTasks: 0,
totalDays: 0,
};
}
static updateProps(file: IDirtyFile, commit: ICommit) {
file.tasks.add(commit.task);
file.timestamp.add(commit.timestamp);
}
static updateTotal(file: IDirtyFile) {
file.totalTasks = file.tasks.size;
file.totalDays = file.timestamp.size;
}
}

View file

@ -0,0 +1,51 @@
import { IDirtyFile } from 'ts/interfaces/FileInfo';
type ArrayIdFile = [string, IDirtyFile];
function isCorrectFile(file: IDirtyFile) {
return !(
file.action === 'D'
|| file.path.length < 3
|| !file.extension
|| { json: true, xml: true, md: true, config: true }[file.extension]
|| { test: true, config: true }[file.type]
);
}
function getFilesByProperty(
list: IDirtyFile[],
property: string,
): ArrayIdFile[] {
list.sort((a: IDirtyFile, b: IDirtyFile) => b[property] - a[property]);
return list
.slice(0, 20)
.map((file: IDirtyFile) => [file.id, file]);
}
export default class FileGripByRefactor {
files: IDirtyFile[] = [];
updateTotalInfo(files: IDirtyFile[]) {
const list = files.filter(isCorrectFile);
const filesWithProblems = [
...getFilesByProperty(list, 'lines'),
...getFilesByProperty(list, 'totalDays'),
...getFilesByProperty(list, 'totalTasks'),
];
const uniqueFilesWithProblems = Array.from(
(new Map<string, IDirtyFile>(filesWithProblems)).values(),
).filter((file: IDirtyFile) => (
file.lines > 50 && file.totalDays > 10
));
uniqueFilesWithProblems.sort((a: any, b: any) => b.totalDays - a.totalDays);
this.files = uniqueFilesWithProblems;
}
clear() {
this.files = [];
}
}

View file

@ -4,6 +4,7 @@ import IHashMap from 'ts/interfaces/HashMap';
import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
import getInfoFromNameAndEmail from './getCompany';
import { getGithubPrInfo } from './getMergeInfo';
import getCountryByTimeZone from './getCountryByTimeZone';
const MASTER_BRANCH = {
master: true,
@ -47,13 +48,17 @@ export default function getCommitInfo(
let email = parts[2] || '';
if (email.indexOf('@') === -1) email = '';
const companyKey = `${author}>in>${email}`;
if (!refEmailAuthor[companyKey]) {
// @ts-ignore
const companyKey = `${author}>mail>${email}`;
if (!refEmailAuthor[companyKey]) { // @ts-ignore
refEmailAuthor[companyKey] = getInfoFromNameAndEmail(author, email);
}
// @ts-ignore
const { company, country, device } = refEmailAuthor[companyKey];
} // @ts-ignore
const { company, domain, device } = refEmailAuthor[companyKey];
const countryKey = `${author}>time>${timezone}`;
if (!refEmailAuthor[countryKey]) {// @ts-ignore
refEmailAuthor[countryKey] = getCountryByTimeZone(timezone, domain, author);
} // @ts-ignore
const country = refEmailAuthor[countryKey];
const authorID = author.replace(/\s|\t/gm, '');
if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) {

View file

@ -1,4 +1,3 @@
import getCountryByDomain from './getCountryByDomain';
import getDevice from './getDevice';
const PUBLIC_SERVICES = [
@ -22,6 +21,7 @@ const PUBLIC_SERVICES = [
'me',
'qq',
'dev',
'list',
'localhost',
];
@ -73,7 +73,6 @@ function isUserName(author?: string, company?: string): boolean {
export default function getInfoFromNameAndEmail(author?: string, email?: string) {
const companyByAuthor = getCompanyByName(author);
const [companyByEmail, domain] = getCompanyAndDomainByEmail(email);
const country = getCountryByDomain(domain);
const device = getDevice(companyByEmail);
const companyName = companyByAuthor || companyByEmail || '';
@ -84,5 +83,5 @@ export default function getInfoFromNameAndEmail(author?: string, email?: string)
|| isIP.test(companyName);
const company = (!isInCorrect && !device) ? companyName : '';
return { company, country, device };
return { company, domain, device };
}

View file

@ -0,0 +1,253 @@
export const REF_DOMAIN_COUNTRY = {
ac: 'Ascension Island',
ad: 'Andorra',
ae: 'United Arab Emirates',
af: 'Afghanistan',
ag: 'Antigua and Barbuda',
ai: 'Anguilla',
al: 'Albania',
am: 'Armenia',
ao: 'Angola',
aq: 'Antarctica',
ar: 'Argentina',
as: 'American Samoa',
at: 'Austria',
au: 'Australia',
aw: 'Aruba (Kingdom of the Netherlands)',
ax: 'Aland (Finland)',
az: 'Azerbaijan',
ba: 'Bosnia and Herzegovina',
bb: 'Barbados',
bd: 'Bangladesh',
be: 'Belgium',
bf: 'Burkina Faso',
bg: 'Bulgaria',
bh: 'Bahrain',
bi: 'Burundi',
bj: 'Benin',
bm: 'Bermuda',
bn: 'Brunei',
bo: 'Bolivia',
bq: 'Caribbean Netherlands',
br: 'Brazil',
bs: 'Bahamas',
bt: 'Bhutan',
bw: 'Botswana',
by: 'Belarus',
bz: 'Belize',
ca: 'Canada',
cc: 'Cocos (Keeling) Islands',
cd: 'Democratic Republic of the Congo',
cf: 'Central African Republic',
cg: 'Republic of the Congo',
ch: 'Switzerland',
ci: 'Ivory Coast',
ck: 'Cook Islands',
cl: 'Chile',
cm: 'Cameroon',
cn: 'China',
co: 'Colombia',
cr: 'Costa Rica',
cu: 'Cuba',
cv: 'Cape Verde',
cw: 'Curacao (Kingdom of the Netherlands)',
cx: 'Christmas Island',
cy: 'Cyprus',
cz: 'Czech Republic',
de: 'Germany',
dj: 'Djibouti',
dk: 'Denmark',
dm: 'Dominica',
do: 'Dominican Republic',
dz: 'Algeria',
ec: 'Ecuador',
ee: 'Estonia',
eg: 'Egypt',
eh: 'Western Sahara',
er: 'Eritrea',
es: 'Spain',
et: 'Ethiopia',
eu: 'Europe',
fi: 'Finland',
fj: 'Fiji',
fk: 'Falkland Islands',
fo: 'Faroe Islands (Kingdom of Denmark)',
fr: 'France',
ga: 'Gabon',
gd: 'Grenada',
ge: 'Georgia',
gf: 'French Guiana',
gg: 'Guernsey',
gh: 'Ghana',
gi: 'Gibraltar',
gl: 'Greenland (Kingdom of Denmark)',
gm: 'The Gambia',
gn: 'Guinea',
gp: 'Guadeloupe',
gq: 'Equatorial Guinea10 July 1997',
gr: 'Greece',
gs: 'United Kingdom',
gt: 'Guatemala',
gu: 'Guam',
gw: 'Guinea-Bissau',
gy: 'Guyana',
hk: 'Hong Kong',
hm: 'Heard Island and McDonald Islands',
hn: 'Honduras',
hr: 'Croatia',
ht: 'Haiti',
hu: 'Hungary',
id: 'Indonesia',
ie: 'Ireland',
il: 'Israel',
im: 'Isle of Man',
in: 'India',
iq: 'Iraq',
ir: 'Iran',
is: 'Iceland',
it: 'Italy',
je: 'Jersey',
jm: 'Jamaica',
jo: 'Jordan',
jp: 'Japan',
ke: 'Kenya',
kg: 'Kyrgyzstan',
kh: 'Cambodia',
ki: 'Kiribati',
km: 'Comoros',
kn: 'Saint Kitts and Nevis',
kp: 'North Korea',
kr: 'South Korea',
kw: 'Kuwait',
ky: 'Cayman Islands',
kz: 'Kazakhstan',
la: 'Laos',
lb: 'Lebanon',
lc: 'Saint Lucia',
li: 'Liechtenstein',
lk: 'Sri Lanka',
lr: 'Liberia',
ls: 'Lesotho',
lt: 'Lithuania',
lu: 'Luxembourg',
lv: 'Latvia',
ly: 'Libya',
ma: 'Morocco',
mc: 'Monaco',
md: 'Moldova',
me: 'Montenegro',
mg: 'Madagascar',
mh: 'Marshall Islands',
mk: 'North Macedonia',
ml: 'Mali',
mm: 'Myanmar',
mn: 'Mongolia',
mo: 'Macau',
mp: 'Northern Mariana Islands',
mq: 'Martinique',
mr: 'Mauritania',
ms: 'Montserrat',
mt: 'Malta',
mu: 'Mauritius',
mv: 'Maldives',
mw: 'Malawi',
mx: 'Mexico',
my: 'Malaysia',
mz: 'Mozambique',
na: 'Namibia',
nc: 'New Caledonia',
ne: 'Niger',
nf: 'Norfolk Island',
ng: 'Nigeria',
ni: 'Nicaragua',
nl: 'Netherlands',
no: 'Norway',
np: ' Nepal',
nr: 'Nauru',
nu: 'Niue',
nz: 'New Zealand',
om: 'Oman',
pa: 'Panama',
pe: 'Peru',
pf: 'French Polynesia',
pg: 'Papua New Guinea',
ph: 'Philippines',
pk: 'Pakistan',
pl: 'Poland',
pm: 'Saint-Pierre and Miquelon',
pn: 'Pitcairn Islands',
pr: 'Puerto Rico',
ps: 'Palestine',
pt: 'Portugal',
pw: 'Palau',
py: 'Paraguay',
qa: 'Qatar',
re: 'Réunion',
ro: 'Romania',
rs: 'Serbia',
ru: 'Russia',
rw: 'Rwanda',
sa: 'Saudi Arabia',
sb: 'Solomon Islands',
sc: 'Seychelles',
sd: 'Sudan',
se: 'Sweden',
sg: 'Singapore',
sh: 'Saint Helena, Ascension and Tristan da Cunha',
si: 'Slovenia',
sk: 'Slovakia',
sl: 'Sierra Leone',
sm: 'San Marino',
sn: 'Senegal',
so: 'Somalia',
sr: 'Suriname',
ss: 'South Sudan',
st: 'São Tomé and Príncipe',
sv: 'El Salvador',
sx: 'Sint Maarten',
sy: 'Syria',
sz: 'Eswatini',
tc: 'Turks and Caicos Islands',
td: 'Chad',
tf: 'French Southern and Antarctic Lands',
tg: 'Togo',
th: 'Thailand',
tj: 'Tajikistan',
tl: 'East Timor',
tm: 'Turkmenistan',
tn: 'Tunisia',
to: 'Tonga',
tr: 'Turkey',
tt: 'Trinidad and Tobago',
tw: 'Taiwan',
tz: 'Tanzania',
ua: 'Ukraine',
ug: 'Uganda',
uk: 'United Kingdom',
us: 'USA',
uy: 'Uruguay',
uz: 'Uzbekistan',
va: 'Vatican City',
vc: 'Saint Vincent and the Grenadines',
ve: 'Venezuela',
vg: 'British Virgin Islands',
vi: 'United States Virgin Islands',
vn: 'Vietnam',
vu: 'Vanuatu',
wf: 'Wallis and Futuna',
ws: 'Samoa',
ye: 'Yemen',
yt: 'Mayotte',
za: 'South Africa',
zm: 'Zambia',
zw: 'Zimbabwe',
africa: 'African Union',
};
export const REF_NEW_OLD_DOMAIN = {
su: 'ru',
gov: 'us',
mil: 'us',
amazon: 'us',
aws: 'us',
};

View file

@ -0,0 +1,168 @@
import {
REF_DOMAIN_COUNTRY,
REF_NEW_OLD_DOMAIN,
} from './getCountryByDomain';
const FAMILY = {
ru: ['а', 'е', 'и', 'ivan', 'alexan', 'alexe', 'petr', 'konstan', 'sergey', 'dmitr', 'roman', 'pavel', 'vlad', 'nikol', 'maks', 'andre', 'oleg', 'denis', 'victor', 'eugen', 'ikhail', 'italy', 'yura', 'igor'],
tr: ['ilmaz', 'aya', 'demir', 'elik', 'ahin', 'ildiz', 'ildirım'],
pt: ['silva', 'santos', 'ferreira', 'pereira', 'oliveira', 'rodrigues', 'pereira', 'soares'],
kr: ['kim', 'won', 'khan'],
jp: ['sudzuki', 'yashi', 'kami', 'yuki', 'yama', 'sato', 'mato', 'moto', 'sawa', 'hiro'],
uk: ['watson', 'thomson', 'smith', 'johnson', 'williams', 'jones', 'brown', 'davis', 'miller', 'wilson', 'moore', 'taylor'],
es: ['ñ', 'gonzales', 'rodriguez', 'fernandez', 'garcia', 'lopez'],
fr: ['blanchet', 'boucher', 'deschamps', 'dupont', 'fournier', 'garnier', 'laurent', 'lavigne', 'martin', 'monet'],
it: ['rossi', 'ferrari', 'conti', 'romano', 'bruni', 'esposito', 'russo', 'marino', 'de luca', 'mancini'],
pl: ['ł'],
ee: ['õ'],
il: ['ה', 'י', 'ו'],
};
const LINE = {
'-12:00': { countries: ['us'] },
'-11:00': { countries: ['us'] },
'-10:00': { countries: ['us'] },
'-09:30': { countries: ['pf'] },
'-09:00': { countries: ['us'] },
'-08:00': { countries: ['us'] },
'-07:00': { countries: ['ca', 'us'] },
'-06:00': { countries: ['ca', 'us', 'mx', 'hn', 'ni'] },
'-05:00': {
title: 'Canada or USA or Caribbean or Peru',
countries: ['ca', 'us', 'jm', 'dm', 'pa', 'pe', 'cu', 'co'],
},
'-04:00': { countries: ['ca', 'us', 'South America'] },
'-03:00': {
countries: ['ar', 'br'],
name: {
ar: FAMILY.es,
br: FAMILY.pt,
},
},
'-02:00': { countries: ['br'] }, // not gl
'-01:00': { countries: ['pt'] },
'+00:00': {
countries: ['pt', 'uk'],
name: {
pt: FAMILY.pt,
uk: FAMILY.uk,
ru: FAMILY.ru,
},
},
'+01:00': {
title: 'Europe',
countries: [
'uk', 'es', 'fr', 'de', 'it', 'ch', 'at', 'pl', 'be', 'li', 'rs', 'se', 'no', 'me', 'si', 'sk', 'dk', 'nl',
'dz', 'ne', 'td', 'ao',
],
name: {
es: FAMILY.es,
fr: FAMILY.fr,
it: FAMILY.it,
pl: FAMILY.pl,
ee: FAMILY.ee,
},
},
'+02:00': {
title: 'Finland or Ukraine or Balkans or Israel',
countries: [
'fi', 'lv', 'lt', 'ee', 'ua', 'bg', 'gr', 'cy', 'il',
'eg', 'ly', 'za',
],
name: {
es: FAMILY.es,
pl: FAMILY.pl,
ee: FAMILY.ee,
il: FAMILY.il,
},
},
'+03:00': {
countries: ['ru', 'tr'],
name: {
ru: FAMILY.ru,
tr: FAMILY.tr,
},
},
'+03:30': { countries: ['ir'] },
'+04:00': {
countries: ['ru'],
},
'+04:30': { countries: ['af'] },
'+05:00': {
title: 'Yekaterinburg or Middle Asia or Pakistan',
countries: ['ru', 'kz', 'uz', 'uz', 'kg', 'tm', 'tj', 'pk'],
name: {
ru: FAMILY.ru,
},
},
'+05:30': { countries: ['in'] },
'+05:45': { countries: ['np'] },
'+06:00': { countries: ['ru'] },
'+07:00': {
countries: ['th', 'vn', 'id'],
name: {
ru: FAMILY.ru,
},
},
'+08:00': {
title: 'China or Philippines',
countries: ['cn', 'ph', 'ml'],
},
'+09:00': {
countries: ['kr', 'jp'],
name: {
kr: FAMILY.kr,
jp: FAMILY.jp,
},
},
'+09:30': { countries: ['au'] },
'+10:00': { countries: ['au'] },
'+10:30': { countries: ['au'] },
'+11:00': { countries: ['au'] }, // not ru
'+12:00': { countries: ['nz'] },
'+13:00': { countries: ['ws'] },
};
function updateLines() {
let keys = Object.keys(LINE);
keys.forEach((timezone: string, index: number) => {
LINE[timezone].prev = LINE[(keys[index - 1] || '')]?.countries || [];
LINE[timezone].next = LINE[(keys[index + 1] || '')]?.countries || [];
if (!LINE[timezone].title) {
LINE[timezone].title = LINE[timezone].countries.map((id: string) => REF_DOMAIN_COUNTRY[id] || id).join(' or ');
}
});
}
updateLines();
export default function getCountryByTimeZone(
timeZone?: string,
domain?: string,
name?: string,
) {
const data = LINE[timeZone || ''];
if (!data) return '';
if (data.countries.length === 1) {
return REF_DOMAIN_COUNTRY[data.countries[0]];
}
if (domain && [
...data.prev,
...data.countries,
...data.next,
].indexOf(domain) !== -1) {
const newDomain = REF_NEW_OLD_DOMAIN[domain || ''] || domain;
return REF_DOMAIN_COUNTRY[newDomain];
}
if (name && data.name) {
const family = name.toLowerCase();
for (let key in data.name) {
const hasSuffix = data.name[key].some((text: string) => family.indexOf(text) !== -1);
if (hasSuffix) return REF_DOMAIN_COUNTRY[key];
}
}
return data.title;
}

View file

@ -0,0 +1,87 @@
import { HashMap } from 'ts/interfaces/HashMap';
import { ONE_DAY } from 'ts/helpers/formatter';
const BY_X = {
'-12:00': 1,
'-11:00': 2,
'-10:00': 3,
'-09:30': 3.5,
'-09:00': 4,
'-08:00': 5,
'-07:00': 6,
'-06:00': 7,
'-05:00': 8,
'-04:00': 9,
'-03:00': 10,
'-02:00': 11,
'-01:00': 12,
'+00:00': 13,
'+01:00': 14,
'+02:00': 15,
'+03:00': 16,
'+03:30': 16.5,
'+04:00': 17,
'+04:30': 17.5,
'+05:00': 18,
'+05:30': 18.5,
'+05:45': 18.75,
'+06:00': 19,
'+07:00': 20,
'+08:00': 21,
'+09:00': 22,
'+09:30': 22.5,
'+10:00': 23,
'+10:30': 23.5,
'+11:00': 24,
'+12:00': 25,
'+13:00': 26,
};
function getDistance(timezoneA: string, timezoneB: string) {
return Math.abs(BY_X[timezoneA] - BY_X[timezoneB]);
}
export function getVpnList(countries: any[]) {
const vpn = new Map();
if (countries.length < 2) return vpn;
for (let i = 0, l = countries.length; i < l; i++) {
const from = countries[i];
const to = countries[i + 1];
const next = countries[i + 2];
if (!to || !next) continue;
const isFast = (next?.from - from?.from) < ONE_DAY;
const isThisPlace = from.timezone === next.timezone;
if (isFast && isThisPlace) {
vpn.set(to.country, to.timezone);
i += 1;
}
}
return vpn;
}
export function getTravels(countries: any[], vpnList: HashMap<string>) {
if (countries.length === 1) return null;
let from = countries[0].timezone;
const path = [countries[0]];
const formattedCountries = countries.length > 3
? countries.filter((item: any) => !vpnList.has(item.country))
: countries;
for (let i = 1, l = formattedCountries.length; i < l; i++) {
const country = formattedCountries[i];
const to = country.timezone;
if (from === to) continue;
if (getDistance(from, to) > 1) {
from = to;
path.push(country);
}
}
return (path.length === 1) ? null : path;
}

View file

@ -0,0 +1,5 @@
export default function getDevice(company?: string) {
return company && (/(MACBOOK)|(-AIR)|(-IMAC)/gi).test(company)
? 'MacBook'
: '';
}

View file

@ -0,0 +1,14 @@
// "Merge pull request #3 in repository from TASK-123-add-profile to master"
// "Merge pull request #3 from facebook/compiler into master"
// "Merge pull request #3 from facebook/compiler"
export function getGithubPrInfo(text: string) {
const json = (text || '')
.replace(/"/gim, '')
.replace('#', '#": "')
.replace(' in ', '", "in": "')
.replace(' from ', '", "from": "')
.replace(' to ', '", "to": "')
.replace(' into ', '", "to": "');
const data = JSON.parse(`{"${json}"}`);
return [data['Merge pull request #'], data.in, data.from, data.to];
}

View file

@ -0,0 +1,65 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import PageWrapper from 'ts/components/Page/wrapper';
import PageColumn from 'ts/components/Page/column';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import PieChart from 'ts/components/PieChart';
import { STATUS, WORK_DAYS } from '../contstants';
import { increment } from 'ts/helpers/Math';
function getStatusChart(rows: any[]) {
const order = Object.values(STATUS);
const options = getOptions({ order, limit: 1 });
const details = rows.reduce((acc: any, row: any) => {
if (row.isStaff) increment(acc, STATUS.STAFF);
else if (row.isDismissed) increment(acc, STATUS.DISMISSED);
else increment(acc, STATUS.WORK);
return acc;
}, {});
return [options, details];
}
function getDaysChart(rows: any[]) {
const order = Object.values(WORK_DAYS);
const options = getOptions({ order, limit: 1, suffix: 'page.team.author.daysChart.item' });
const details = rows.reduce((acc: any, row: any) => {
if (row.daysAll < 183) increment(acc, WORK_DAYS.HALF);
else if (row.daysAll < 365) increment(acc, WORK_DAYS.ONE);
else if (row.daysAll < 547) increment(acc, WORK_DAYS.HALF_ONE);
else if (row.daysAll < 730) increment(acc, WORK_DAYS.TWO);
else increment(acc, WORK_DAYS.MORE);
return acc;
}, {});
return [options, details];
}
const PieCharts = observer((): React.ReactElement | null => {
const rows = dataGripStore.dataGrip.author.statistic;
const [statusOptions, statusDetails] = getStatusChart(rows);
const [daysOptions, daysDetails] = getDaysChart(rows);
return (
<PageWrapper>
<PageColumn>
<PieChart
title="page.team.author.statusChart.title"
options={statusOptions}
details={statusDetails}
/>
</PageColumn>
<PageColumn>
<PieChart
title="page.team.author.daysChart.title"
options={daysOptions}
details={daysDetails}
/>
</PageColumn>
</PageWrapper>
);
});
export default PieCharts;

View file

@ -0,0 +1,201 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import { IPagination } from 'ts/interfaces/Pagination';
import { getDate, getMoney, getShortNumber } from 'ts/helpers/formatter';
import dataGripStore from 'ts/store/DataGrip';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import { getMax, getMaxByLength } from 'ts/pages/Common/helpers/getMax';
interface ViewProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
export function View({ response, updateSort, rowsForExcel, mode }: ViewProps) {
const { t } = useTranslation();
if (!response) return null;
const [works, dismissed, staff] = [
t('page.team.author.type.work'),
t('page.team.author.type.dismissed'),
t('page.team.author.type.staff'),
];
const textWork = t('page.team.author.worked');
const textLosses = t('page.team.author.losses');
const daysWorked = getOptions({ order: [textWork, textLosses], suffix: 'page.team.author.days' });
const taskChart = getOptions({ max: getMaxByLength(response, 'tasks'), suffix: 'page.team.author.tasksSmall' });
const commitsChart = getOptions({ max: getMax(response, 'commits') });
const typeChart = getOptions({ order: dataGripStore.dataGrip.type.list });
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode={mode}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.STRING}
formatter={(row: any, index: number) => (index + 1)}
width={40}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="author"
title="page.team.pr.author"
width={200}
/>
<Column
title="page.team.author.status"
formatter={(row: any) => {
if (row.isStaff) return staff;
if (row.isDismissed) return dismissed;
return works;
}}
template={(value: string) => <UiKitTags value={value} />}
width={100}
/>
<Column
isSortable="company"
title="page.team.author.company"
properties="lastCompany"
template={(value: string) => <UiKitTags value={value} />}
width={150}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="firstCommit"
title="page.team.author.firstCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="lastCommit"
title="page.team.author.lastCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
title="page.team.author.daysAll"
properties="daysAll"
formatter={(value: number) => value || 1}
width={90}
/>
<Column
isSortable="daysWorked"
title="page.team.author.workedLosses"
minWidth={300}
template={(details: any) => (
<LineChart
options={daysWorked}
details={details}
/>
)}
formatter={(row: any) => {
return { [textWork]: row.daysWorked, [textLosses]: row.daysLosses };
}}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="tasks"
formatter={(tasks: string[]) => (tasks?.length || 0)}
/>
<Column
isSortable
properties="tasks"
title="page.team.author.tasks"
minWidth={200}
template={(value: number) => (
<LineChart
options={taskChart}
value={value}
/>
)}
formatter={(tasks: any) => (tasks?.length || 0)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
title="page.team.author.daysForTask"
properties="daysForTask"
formatter={getShortNumber}
width={120}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
title="page.team.author.scopes"
properties="scopes"
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="commits"
/>
<Column
isSortable
title="page.team.author.commits"
properties="commits"
minWidth={100}
template={(value: number) => (
<LineChart
options={commitsChart}
value={value}
/>
)}
/>
<Column
title="page.team.author.types"
properties="types"
width={400}
template={(details: IHashMap<number>) => (
<LineChart
options={typeChart}
details={details}
/>
)}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyAll"
properties="moneyAll"
formatter={getMoney}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyWorked"
properties="moneyWorked"
formatter={getMoney}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyLosses"
properties="moneyLosses"
formatter={getMoney}
/>
</DataView>
);
}
View.defaultProps = {
response: undefined,
};
export default View;

View file

@ -0,0 +1,15 @@
import { t } from 'ts/helpers/Localization';
export const STATUS = {
WORK: t('page.team.author.type.work'),
DISMISSED: t('page.team.author.type.dismissed'),
STAFF: t('page.team.author.type.staff'),
};
export const WORK_DAYS = {
HALF: t('page.team.author.days.half'),
ONE: t('page.team.author.days.one'),
HALF_ONE: t('page.team.author.days.15'),
TWO: t('page.team.author.days.two'),
MORE: t('page.team.author.days.more'),
};

View file

@ -0,0 +1,78 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
import ISort from 'ts/interfaces/Sort';
import { IPaginationRequest } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import PageWrapper from 'ts/components/Page/wrapper';
import PageColumn from 'ts/components/Page/column';
import DataLoader from 'ts/components/DataLoader';
import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title';
import Recommendations from 'ts/components/Recommendations';
import Description from 'ts/components/Description';
import PieCharts from './components/PieCharts';
import View from './components/View';
const Author = observer(({
mode,
}: ICommonPageProps): React.ReactElement | null => {
const { t } = useTranslation();
const rows = dataGripStore.dataGrip.author.statistic;
if (!rows?.length) {
return mode !== 'print' ? (<NothingFound />) : null;
}
const recommendations = dataGripStore.dataGrip.recommendations.team?.byAuthor;
return (
<>
{mode !== 'fullscreen' && (
<Recommendations
mode={mode}
recommendations={recommendations}
/>
)}
<br />
<br />
<PieCharts />
<Title title="page.team.author.title"/>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
content: rows, pagination, sort, mode,
})}
watch={`${mode}${dataGripStore.hash}`}
>
<View
mode={mode}
rowsForExcel={rows}
/>
<Pagination />
</DataLoader>
<PageWrapper>
<PageColumn>
<Description
text={t('page.team.author.description1')}
/>
</PageColumn>
<PageColumn>
<Description
text={t('page.team.author.description2')}
/>
</PageColumn>
</PageWrapper>
</>
);
});
export default Author;

View file

@ -0,0 +1,63 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import PageWrapper from 'ts/components/Page/wrapper';
import PageColumn from 'ts/components/Page/column';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import PieChart from 'ts/components/PieChart';
import { WORK_DAYS } from '../../Author/contstants';
import { increment } from 'ts/helpers/Math';
function getStatusChart(rows: any[]) {
const order = rows.map((data: any) => data.company);
const limit = order.length > 10 ? 2 : 1;
const options = getOptions({ order, limit, suffix: 'page.team.company.employments.item' });
const details = Object.fromEntries(
rows.map((row: any) => [row.company, row.employments.length]),
);
return [options, details];
}
function getDaysChart(rows: any[]) {
const order = Object.values(WORK_DAYS);
const options = getOptions({ order, limit: 1, suffix: 'page.team.company.daysChart.item' });
const details = rows.reduce((acc: any, row: any) => {
if (row.totalDays < 183) increment(acc, WORK_DAYS.HALF);
else if (row.totalDays < 365) increment(acc, WORK_DAYS.ONE);
else if (row.totalDays < 547) increment(acc, WORK_DAYS.HALF_ONE);
else if (row.totalDays < 730) increment(acc, WORK_DAYS.TWO);
else increment(acc, WORK_DAYS.MORE);
return acc;
}, {});
return [options, details];
}
const PieCharts = observer((): React.ReactElement | null => {
const rows = dataGripStore.dataGrip.company.statistic;
const [statusOptions, statusDetails] = getStatusChart(rows);
const [daysOptions, daysDetails] = getDaysChart(rows);
return (
<PageWrapper>
<PageColumn>
<PieChart
title="page.team.company.employments.title"
options={statusOptions}
details={statusDetails}
/>
</PageColumn>
<PageColumn>
<PieChart
title="page.team.company.daysChart.title"
options={daysOptions}
details={daysDetails}
/>
</PageColumn>
</PageWrapper>
);
});
export default PieCharts;

View file

@ -0,0 +1,135 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { IPagination } from 'ts/interfaces/Pagination';
import { getDate } from 'ts/helpers/formatter';
import dataGripStore from 'ts/store/DataGrip';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import { getMax } from 'ts/pages/Common/helpers/getMax';
import Employments from './Employments';
interface CompaniesProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Companies({ response, updateSort, rowsForExcel, mode }: CompaniesProps) {
const { t } = useTranslation();
if (!response) return null;
const [works, dismissed] = [
t('page.team.company.active.yes'),
t('page.team.company.active.no'),
];
const taskChart = getOptions({ max: getMax(response, 'tasks'), suffix: 'page.team.author.tasksSmall' });
const daysChart = getOptions({ max: getMax(response, 'totalDays'), suffix: 'page.team.author.days' });
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.DETAILS}
width={40}
formatter={(row: any) => {
const content = row.employments.map((name: string) => (
dataGripStore?.dataGrip?.author?.statisticByName?.[name]
)).filter((v: any) => v);
return (
<Employments // @ts-ignore
response={{ content }}
mode="details"
/>
);
}}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="company"
width={200}
/>
<Column
title="page.team.author.status"
formatter={(row: any) => (row.isActive ? works : dismissed)}
template={(value: string) => <UiKitTags value={value} />}
width={140}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="from"
title="page.team.author.firstCommit"
width={130}
formatter={getDate}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="to"
title="page.team.author.lastCommit"
width={130}
formatter={getDate}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="totalDays"
width={90}
/>
<Column
isSortable
title="page.team.author.daysAll"
properties="totalDays"
width={150}
template={(value: number) => (
<LineChart
options={daysChart}
value={value}
/>
)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="tasks"
width={90}
/>
<Column
isSortable
title="page.team.author.tasks"
properties="tasks"
width={150}
template={(value: number) => (
<LineChart
options={taskChart}
value={value}
/>
)}
/>
<Column
properties="emptyCell"
minWidth={40}
/>
</DataView>
);
}
Companies.defaultProps = {
response: undefined,
};
export default Companies;

View file

@ -0,0 +1,146 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ICommit from 'ts/interfaces/Commit';
import { IPagination } from 'ts/interfaces/Pagination';
import { getDate, getMoney } from 'ts/helpers/formatter';
import dataGripStore from 'ts/store/DataGrip';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
interface EmploymentsProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
export function Employments({ response, updateSort, rowsForExcel, mode }: EmploymentsProps) {
const { t } = useTranslation();
if (!response) return null;
const [works, dismissed, staff] = [
t('page.team.author.type.work'),
t('page.team.author.type.dismissed'),
t('page.team.author.type.staff'),
];
const textWork = t('page.team.author.worked');
const textLosses = t('page.team.author.losses');
const daysWorked = getOptions({ order: [textWork, textLosses], suffix: 'page.team.author.days' });
const typeChart = getOptions({
suffix: 'page.team.author.tasksSmall',
order: dataGripStore.dataGrip.type.list,
});
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode={mode}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.STRING}
formatter={(row: any, index: number) => (index + 1)}
width={40}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="author"
title="page.team.pr.author"
width={158}
/>
<Column
formatter={(row: any) => {
if (row.isStaff) return staff;
if (row.isDismissed) return dismissed;
return works;
}}
template={(value: string) => <UiKitTags value={value} />}
width={140}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="firstCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="lastCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="daysAll"
formatter={(value: number) => value || 1}
width={90}
/>
<Column
isSortable="daysWorked"
width={150}
template={(details: any) => (
<LineChart
options={daysWorked}
details={details}
/>
)}
formatter={(row: any) => {
return { [textWork]: row.daysWorked, [textLosses]: row.daysLosses };
}}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="tasks"
formatter={(tasks: string[]) => (tasks?.length || 0)}
width={90}
/>
<Column
isSortable
width={150}
template={(row: any) => (
<LineChart
options={typeChart}
details={row.types}
/>
)}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyAll"
properties="moneyAll"
formatter={getMoney}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyWorked"
properties="moneyWorked"
formatter={getMoney}
/>
<Column
template={ColumnTypesEnum.NUMBER}
title="page.team.author.moneyLosses"
properties="moneyLosses"
formatter={getMoney}
/>
</DataView>
);
}
Employments.defaultProps = {
response: undefined,
};
export default Employments;

View file

@ -0,0 +1,62 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import PageWrapper from 'ts/components/Page/wrapper';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import PieChart from 'ts/components/PieChart';
import PageColumn from 'ts/components/Page/column';
import { increment } from 'ts/helpers/Math';
function getCountryChart(rows: any[]) {
const order = rows.map((data: any) => data.country);
const limit = order.length > 10 ? 2 : 1;
const options = getOptions({ order, limit, suffix: 'page.team.country.chart.item' });
const details = Object.fromEntries(
rows.map((row: any) => [row.country, row.employments.length]),
);
return [options, details];
}
function getTimeZoneChart(authors: any[]) {
const details = authors.reduce((acc: any, author) => {
increment(acc, author.lastCommit.timezone.replace(':', '.'));
return acc;
}, {});
const options = getOptions({
order: Object.keys(details).sort(),
limit: 5,
suffix: 'page.team.country.chart.item',
});
return [options, details];
}
const PieCharts = observer((): React.ReactElement | null => {
const authors = dataGripStore.dataGrip.author.statistic;
const rows = dataGripStore.dataGrip.country.statistic;
const [countryOptions, countryDetails] = getCountryChart(rows);
const [timezoneOptions, timezoneDetails] = getTimeZoneChart(authors);
return (
<PageWrapper>
<PageColumn>
<PieChart
title="page.team.country.pieByDomain.title"
options={countryOptions}
details={countryDetails}
/>
</PageColumn>
<PageColumn>
<PieChart
title="page.team.country.pieByTimezone.title"
options={timezoneOptions}
details={timezoneDetails}
/>
</PageColumn>
</PageWrapper>
);
});
export default PieCharts;

View file

@ -0,0 +1,90 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import { getMaxByLength } from 'ts/pages/Common/helpers/getMax';
import Employments from '../../Company/components/Employments';
interface CompaniesProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Countries({ response, updateSort, rowsForExcel, mode }: CompaniesProps) {
if (!response) return null;
const employmentsChart = getOptions({ max: getMaxByLength(response, 'employments') });
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.DETAILS}
width={40}
formatter={(row: any) => {
const content = row.employments.map((name: string) => (
dataGripStore?.dataGrip?.author?.statisticByName?.[name]
)).filter((v: any) => v);
return (
<Employments // @ts-ignore
response={{ content }}
mode="details"
/>
);
}}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="country"
title="page.team.country.table.country"
width={200}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="employments"
formatter={(employments: string[]) => employments.length}
/>
<Column
isSortable
properties="employments"
width={200}
template={(employments: any) => (
<LineChart
options={employmentsChart}
value={employments.length}
/>
)}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="employments"
minWidth={300}
title="page.team.country.table.employments"
formatter={(employments: string[]) => employments.join(', ')}
/>
</DataView>
);
}
Countries.defaultProps = {
response: undefined,
};
export default Countries;

View file

@ -0,0 +1,61 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import { getDate } from 'ts/helpers/formatter';
interface TravelProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Fly({ response, updateSort, rowsForExcel, mode }: TravelProps) {
if (!response) return null;
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode={mode}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isSortable
template={ColumnTypesEnum.STRING}
title="page.team.country.travel.date"
properties="from"
formatter={getDate}
width={142}
/>
<Column
template={ColumnTypesEnum.STRING}
width={40}
formatter={() => '✈️'}
/>
<Column
template={ColumnTypesEnum.STRING}
width={72}
properties="timezone"
/>
<Column
template={ColumnTypesEnum.STRING}
properties="country"
title="page.team.country.travel.country"
/>
</DataView>
);
}
Fly.defaultProps = {
response: undefined,
};
export default Fly;

View file

@ -0,0 +1,87 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import LineChart from 'ts/components/LineChart';
import { getMaxByLength } from 'ts/pages/Common/helpers/getMax';
import Fly from './Fly';
interface TravelProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Travel({ response, updateSort, rowsForExcel, mode }: TravelProps) {
if (!response) return null;
const flyChart = getOptions({ max: getMaxByLength(response, 'country'), suffix: 'page.team.country.travel.flyItem' });
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.DETAILS}
width={40}
formatter={(row: any) => {
const content = row?.country;
return (
<Fly // @ts-ignore
response={{ content }}
mode="details"
/>
);
}}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="author"
title="page.team.country.travel.author"
width={200}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="country"
formatter={(country: any) => country.length - 1}
/>
<Column
isSortable
properties="country"
title="page.team.country.travel.fly"
width={200}
template={(value: number) => (
<LineChart
options={flyChart}
value={value}
/>
)}
formatter={(tasks: any) => (tasks?.length || 0)}
/>
<Column
template={ColumnTypesEnum.STRING}
title="page.team.country.travel.path"
formatter={(row: any) => row.country.map((c: any) => c.country).join(' ✈️ ')}
/>
</DataView>
);
}
Travel.defaultProps = {
response: undefined,
};
export default Travel;

View file

@ -0,0 +1,39 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import PageWrapper from 'ts/components/Page/wrapper';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import PieChart from 'ts/components/PieChart';
import { HashMap } from 'ts/interfaces/HashMap';
function getVpnChart(details: HashMap<number>) {
const order = Object.entries(details)
.sort((a: any, b: any) => b[1] - a[1])
.map((a: any) => a[0]);
return getOptions({
order,
limit: 1,
suffix: 'page.team.country.vpn.item',
});
}
const PieCharts = observer((): React.ReactElement | null => {
const vpnDetails = dataGripStore.dataGrip.country.vpn;
const vpnOptions = getVpnChart(vpnDetails);
return (
<PageWrapper>
<PieChart
title="page.team.country.vpn.title"
options={vpnOptions}
details={vpnDetails}
/>
</PageWrapper>
);
});
export default PieCharts;

View file

@ -0,0 +1,76 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import ISort from 'ts/interfaces/Sort';
import { IPaginationRequest } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import DataLoader from 'ts/components/DataLoader';
import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
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 Travel from './components/Travel';
const Country = observer(({
mode,
}: ICommonPageProps): React.ReactElement | null => {
const authors = dataGripStore.dataGrip.author.statistic;
const countryRows = dataGripStore.dataGrip.country.statistic;
const travel = authors.filter((dot: any) => dot?.country?.length)
.sort((a: any, b: any) => b?.country?.length - a?.country?.length);
if (!countryRows?.length) {
return mode !== 'print' ? (<NothingFound/>) : null;
}
return (
<>
<PageWrapper>
<Title title="page.team.country.byTimezone"/>
<TimeZoneMap authors={authors}/>
</PageWrapper>
<CountryCharts/>
<Title title="page.team.country.table.title"/>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
content: countryRows, pagination, sort, mode,
})}
watch={`${mode}${dataGripStore.hash}`}
>
<Countries
mode={mode}
rowsForExcel={countryRows}
/>
<Pagination/>
</DataLoader>
{travel.length ? (
<>
<Title title="page.team.country.travel.title"/>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
content: travel, pagination, sort, mode,
})}
watch={`${mode}${dataGripStore.hash}`}
>
<Travel
mode={mode}
rowsForExcel={countryRows}
/>
<Pagination/>
</DataLoader>
</>
) : null}
</>
);
});
export default Country;

View file

@ -0,0 +1,75 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { PRLink, TaskLink } from 'ts/components/ExternalLink';
interface TasksProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Tasks({ response, updateSort, rowsForExcel, mode }: TasksProps) {
if (!response) return null;
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode="details"
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
isSortable
template={(value: string) => (
<TaskLink task={value} />
)}
title="page.team.tasks.task"
properties="task"
width={120}
/>
<Column
properties="types"
width={100}
template={(value: any) => (
<UiKitTags value={Object.keys(value)} />
)}
/>
<Column
properties="scope"
width={100}
template={(value: any) => (
<UiKitTags value={Object.keys(value)} />
)}
/>
<Column
isSortable
minWidth={80}
template={(row: any) => {
const links = row.prIds.map((id: string) => (
<PRLink
key={id}
prId={id}
/>
));
return (<>{links}</>);
}}
/>
</DataView>
);
}
Tasks.defaultProps = {
response: undefined,
};
export default Tasks;

View file

@ -0,0 +1,84 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import { PRLink } from 'ts/components/ExternalLink';
import { getDate } from 'ts/helpers/formatter';
interface IPRViewProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Anonymous({
response,
updateSort,
rowsForExcel,
mode,
}: IPRViewProps) {
if (!response) return null;
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode={mode}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 2 : undefined}
fullScreenMode="anonymous"
>
{mode === 'print' ? (
<Column
isSortable
properties="prId"
width={140}
/>
) : (
<Column
isSortable
template={(value: string, row: any) => {
return (
<PRLink prId={row?.prId} />
);
}}
properties="prId"
width={120}
/>
)}
<Column
isSortable
template={ColumnTypesEnum.STRING}
title="page.team.pr.date"
properties="dateMerge"
formatter={getDate}
width={130}
/>
<Column
isSortable
template={ColumnTypesEnum.STRING}
title="page.team.pr.mergeAuthor"
properties="author"
/>
<Column
isSortable
template={ColumnTypesEnum.STRING}
title="page.team.pr.branch"
properties="branch"
/>
</DataView>
);
}
Anonymous.defaultProps = {
mode: undefined,
response: undefined,
};
export default Anonymous;

View file

@ -0,0 +1,167 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { IPaginationRequest } from 'ts/interfaces/Pagination';
import ICommit from 'ts/interfaces/Commit';
import ISort from 'ts/interfaces/Sort';
import { IPagination } from 'ts/interfaces/Pagination';
import { getDate } from 'ts/helpers/formatter';
import dataGripStore from 'ts/store/DataGrip';
import DataLoader from 'ts/components/DataLoader';
import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import NothingFound from 'ts/components/NothingFound';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import Title from 'ts/components/Title';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import { getMax } from 'ts/pages/Common/helpers/getMax';
import Tasks from './Files/Tasks';
interface CompaniesProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function View({ response, updateSort, rowsForExcel, mode }: CompaniesProps) {
if (!response) return null;
const linesChart = getOptions({ max: getMax(response, 'lines'), suffix: 'page.team.refactor.lines' });
const taskChart = getOptions({ max: getMax(response, 'totalTasks'), suffix: 'page.team.refactor.tasks' });
const daysChart = getOptions({ max: getMax(response, 'totalDays'), suffix: 'page.team.author.days' });
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.DETAILS}
width={40}
properties="tasks"
formatter={(row: any) => {
const content = Array.from(row?.tasks)
.reverse()
.map((taskId: any) => dataGripStore.dataGrip.tasks.statisticByName.get(taskId))
.filter(v => v);
return (
<Tasks // @ts-ignore
response={{ content }}
mode="details"
/>
);
}}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
properties="pathString"
title="page.team.refactor.path"
width={400}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="firstCommit"
title="page.team.refactor.firstCommit"
width={130}
formatter={(commit: ICommit) => getDate(commit.timestamp)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="lines"
width={90}
/>
<Column
isSortable="lines"
title="page.team.refactor.totalLines"
properties="lines"
minWidth={150}
template={(value: number) => (
<LineChart
options={linesChart}
value={value}
/>
)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="totalDays"
width={90}
/>
<Column
isSortable="totalDays"
title="page.team.refactor.totalDays"
properties="totalDays"
minWidth={150}
template={(value: number) => (
<LineChart
options={daysChart}
value={value}
/>
)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="totalTasks"
width={90}
/>
<Column
isSortable="totalTasks"
title="page.team.refactor.totalTasks"
properties="totalTasks"
minWidth={150}
template={(value: number) => (
<LineChart
options={taskChart}
value={value}
/>
)}
/>
</DataView>
);
}
View.defaultProps = {
response: undefined,
};
const Refactor = observer(({
mode,
}: ICommonPageProps): React.ReactElement | null => {
const content = dataGripStore.fileGrip.refactor.files;
if (!content?.length) {
return <NothingFound />;
}
return (
<>
<Title title="page.team.refactor.title"/>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
content, pagination, sort, mode,
})}
>
<View />
<Pagination />
</DataLoader>
</>
);
});
export default Refactor;

View file

@ -0,0 +1,124 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import LineChart from 'ts/components/LineChart';
import { getMax } from 'ts/pages/Common/helpers/getMax';
import { getDate } from 'ts/helpers/formatter';
import AllPR from '../PR/All';
interface ViewProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function View({ response, updateSort, rowsForExcel, mode }: ViewProps) {
if (!response) return null;
const delay = getMax(response, 'delayInDays');
const waiting = getMax(response, 'waitingInDays');
const max = Math.max(delay, waiting);
const delayChart = getOptions({ max, suffix: 'page.team.release.chart' });
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.DETAILS}
width={40}
formatter={(row: any) => {
const content = row.prIds.map((prId: string) => (
dataGripStore?.dataGrip?.pr?.pr?.get(prId)
)).filter((v: any) => v);
return (
<AllPR // @ts-ignore
response={{ content }}
mode="details"
/>
);
}}
/>
<Column
isFixed
template={ColumnTypesEnum.STRING}
title="page.team.release.title"
properties="title"
width={200}
/>
<Column
template={ColumnTypesEnum.STRING}
title="page.team.release.from"
width={150}
properties="from"
formatter={getDate}
/>
<Column
template={ColumnTypesEnum.STRING}
title="page.team.release.to"
width={150}
properties="to"
formatter={getDate}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
title="page.team.release.prLength"
properties="prLength"
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="delayInDays"
/>
<Column
isSortable
title="page.team.release.delay"
properties="delayInDays"
width={170}
minWidth={170}
template={(value: number) => (
<LineChart
options={delayChart}
value={value}
/>
)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="waitingInDays"
/>
<Column
isSortable
title="page.team.release.waiting"
properties="waitingInDays"
width={170}
minWidth={170}
template={(value: number) => (
<LineChart
options={delayChart}
value={value}
/>
)}
/>
</DataView>
);
}
View.defaultProps = {
response: undefined,
};
export default View;

View file

@ -0,0 +1,58 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
import { IPaginationRequest } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import DataLoader from 'ts/components/DataLoader';
import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import UiKitButton from 'ts/components/UiKit/components/Button';
import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title';
import View from './View';
import saveChangeLog from './saveChangeLog';
import style from '../../styles/release.module.scss';
const Release = observer(({
mode,
}: ICommonPageProps): React.ReactElement | null => {
const { t } = useTranslation();
const rows = dataGripStore.dataGrip.release.statistic;
if (rows?.length < 2) return mode !== 'print' ? (<NothingFound />) : null;
return (
<>
{mode === 'print' ? (
<Title title="sidebar.team.extension"/>
) : (
<UiKitButton
mode={['slim']}
className={style.team_release_download}
onClick={saveChangeLog}
>
{t('page.team.release.download')}
</UiKitButton>
)}
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest) => getFakeLoader({
content: rows, pagination, mode,
})}
watch={`${mode}${dataGripStore.hash}`}
>
<View
mode={mode}
rowsForExcel={rows}
/>
<Pagination />
</DataLoader>
</>
);
});
export default Release;

View file

@ -0,0 +1,61 @@
import { downloadFile } from 'ts/helpers/File';
import { getDateForExcel } from 'ts/helpers/formatter';
import dataGripStore from 'ts/store/DataGrip';
import userSettings from 'ts/store/UserSettings';
function groupByType(prs: any[]) {
return prs.reduce((acc: any, item: any) => {
const type = item.type || '';
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(item);
return acc;
}, {});
}
function getTaskDescription(pr: any) {
const message = pr.message.substring(pr.message.lastIndexOf(':') + 2)
.replace(pr.task, '')
.trim();
const prefix = userSettings?.settings?.linksPrefix?.task || '/';
const formattedTask = pr.task?.[0] === '#'
? pr.task.replace('#', '')
: pr.task;
return `- [${formattedTask}](${prefix}${formattedTask}) ${message}`;
}
function getReleaseDescription(prs: any) {
const types = groupByType(prs);
return Object.keys(types)
.sort()
.map((type: string) => {
const tasks = types[type].map(getTaskDescription).join('\n');
if (!type) return `\n${tasks}`;
return `\n### ${type}\n${tasks}`;
}).join('\n');
}
function getChangeLogString() {
const rows = dataGripStore.dataGrip.release.statistic;
const list = rows.map((release: any) => {
const date = getDateForExcel(release.lastCommit.date);
const prs = release.pr
.map((prId: string) => dataGripStore.dataGrip.pr.pr.get(prId))
.filter((v: any) => v);
const description = getReleaseDescription(prs);
return `
## [${release.title}] - ${date}
${description}`;
}).join('\n');
return `# Change Log
${list}`;
}
export default function saveChangeLog() {
const content = getChangeLogString();
const type = 'text/csv;charset=windows-utf-8;'; // utf-8;';
const file = new Blob([content], { type });
downloadFile(file, 'CHANGELOG.md');
}

View file

@ -0,0 +1,60 @@
import React, { useMemo } from 'react';
import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
import SelectWithButtons from 'ts/components/UiKit/components/SelectWithButtons';
import dataGripStore from 'ts/store/DataGrip';
import style from '../../styles/filters.module.scss';
function getFormattedUsers(rows: any[], t: Function) {
const options = rows.map((title: string, id: number) => ({ id: id + 1, title }));
options.unshift({ id: 0, title: t('page.team.tree.filters.all') });
return options;
}
interface ITempoFiltersProps {
filters: {
company?: number;
user?: number;
};
onChange: Function;
}
const TasksFilters = observer(({
filters,
onChange,
}: ITempoFiltersProps): React.ReactElement => {
const { t } = useTranslation();
const users = dataGripStore.dataGrip.author.list;
const userOptions = useMemo(() => getFormattedUsers(users, t), [users]);
const companies = dataGripStore.dataGrip.company.statistic.map((v: any) => v.company);
const companyOptions = useMemo(() => getFormattedUsers(companies, t), [companies]);
return (
<div className={style.table_filters}>
<SelectWithButtons
title="page.team.tree.filters.author"
value={filters.user}
className={style.table_filters_item}
options={userOptions}
onChange={(user: number) => {
onChange({ ...filters, user, company: 0 });
}}
/>
<SelectWithButtons
title="page.team.tree.filters.author"
value={filters.company}
className={style.table_filters_item}
options={companyOptions}
onChange={(company: number) => {
onChange({ ...filters, user: 0, company });
}}
/>
</div>
);
});
export default TasksFilters;

View file

@ -0,0 +1,69 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import IHashMap from 'ts/interfaces/HashMap';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { PRLink } from 'ts/components/ExternalLink';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import { getDate } from 'ts/helpers/formatter';
interface ReleaseProps {
isCorrectPR?: IHashMap<boolean>;
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function Release({ isCorrectPR, response, updateSort, rowsForExcel, mode }: ReleaseProps) {
if (!response) return null;
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode="details"
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.STRING}
title="page.team.release.title"
properties="title"
width={120}
/>
<Column
template={ColumnTypesEnum.STRING}
title="page.team.release.to"
width={198}
formatter={(row: any) => getDate(row.to || row.from)}
/>
<Column
isSortable
minWidth={80}
template={(row: any) => {
const links = row?.prIds
?.filter((id: string) => isCorrectPR?.[id])
?.map((id: string) => (
<PRLink
key={id}
prId={id}
/>
));
return (<>{links}</>);
}}
/>
</DataView>
);
}
Release.defaultProps = {
response: undefined,
};
export default Release;

View file

@ -0,0 +1,126 @@
import React from 'react';
import { IPagination } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import DataView from 'ts/components/DataView';
import Column from 'ts/components/Table/components/Column';
import { ColumnTypesEnum } from 'ts/components/Table/interfaces/Column';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import { PRLink, TaskLink } from 'ts/components/ExternalLink';
import { getDate } from 'ts/helpers/formatter';
import Release from './Release';
interface ViewProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
export function View({ response, updateSort, rowsForExcel, mode }: ViewProps) {
if (!response) return null;
return (
<DataView
rowsForExcel={rowsForExcel}
rows={response.content}
sort={response.sort}
updateSort={updateSort}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
<Column
isFixed
template={ColumnTypesEnum.DETAILS}
width={40}
properties="releaseIds"
formatter={(row: any) => {
const content = Array.from(row?.releaseIds)
.reverse()
.map((id: any) => dataGripStore.dataGrip.release.release[id])
.filter(v => v);
const isCorrectPr = Object.fromEntries(
row.prIds.map((id: string) => [id, true]),
);
return (
<Release // @ts-ignore
response={{ content }}
isCorrectPR={isCorrectPr}
mode="details"
/>
);
}}
/>
<Column
isFixed
isSortable
template={(value: string) => (
<TaskLink task={value} />
)}
title="page.team.tasks.task"
properties="task"
width={120}
/>
<Column
properties="types"
width={100}
template={(value: any) => (
<UiKitTags value={Object.keys(value)} />
)}
/>
<Column
properties="scope"
width={100}
template={(value: any) => (
<UiKitTags value={Object.keys(value)} />
)}
/>
<Column
isSortable
width={80}
template={(row: any) => {
const links = row.prIds.map((id: string) => (
<PRLink
key={id}
prId={id}
/>
));
return (<>{links}</>);
}}
/>
<Column
template={ColumnTypesEnum.STRING}
properties="comments"
/>
<Column
template={ColumnTypesEnum.STRING}
title="page.team.tasks.author"
properties="author"
width={170}
/>
<Column
template={ColumnTypesEnum.STRING}
title="page.team.tasks.from"
properties="from"
width={150}
formatter={getDate}
/>
<Column
template={ColumnTypesEnum.STRING}
title="page.team.tasks.to"
properties="to"
width={150}
formatter={getDate}
/>
</DataView>
);
}
View.defaultProps = {
response: undefined,
};
export default View;

View file

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import ISort from 'ts/interfaces/Sort';
import { IPaginationRequest } from 'ts/interfaces/Pagination';
import dataGripStore from 'ts/store/DataGrip';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import DataLoader from 'ts/components/DataLoader';
import Pagination from 'ts/components/DataLoader/components/Pagination';
import getFakeLoader from 'ts/components/DataLoader/helpers/formatter';
import NothingFound from 'ts/components/NothingFound';
import Title from 'ts/components/Title';
import PageWrapper from 'ts/components/Page/wrapper';
import Filters from './Filters';
import View from './View';
const Tasks = observer(({
mode,
}: ICommonPageProps): React.ReactElement | null => {
const rows = dataGripStore.dataGrip.tasks.statistic;
const [filters, setFilters] = useState<any>({ user: 0, company: 0 });
if (!rows?.length) return mode !== 'print' ? (<NothingFound />) : null;
return (
<>
<Title title="common.filters" />
<PageWrapper>
<Filters
filters={filters}
onChange={setFilters}
/>
</PageWrapper>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
content: rows, pagination, sort, mode,
})}
watch={`${mode}${dataGripStore.hash}`}
>
<View
mode={mode}
rowsForExcel={rows}
/>
<Pagination />
</DataLoader>
</>
);
});
export default Tasks;

View file

@ -0,0 +1,6 @@
@import 'src/styles/variables';
.team_release_download {
position: relative;
top: -12px;
}

View file

@ -134,8 +134,14 @@ export default `
§ page.team.country.pieByTimezone.title: По времени
§ page.team.country.chart.item: сотрудников
§ page.team.country.table.title: Список сотрудников
§ page.team.country.table.country: Местоположение
§ page.team.country.table.country: Локация
§ page.team.country.table.employments: Сотрудники
§ page.team.country.travel.title: Командировки (или VPN, или rebase)
§ page.team.country.travel.author: Сотрудник
§ page.team.country.travel.fly: Количество перелётов
§ page.team.country.travel.path: Список локаций
§ page.team.country.travel.date: Дата перлёта
§ page.team.country.travel.country: Локация
§ page.team.refactor.title: Кандидаты на рефакторинг
§ page.team.refactor.lines: строк
§ page.team.refactor.tasks: задач