update
BIN
build/assets/map/2x1.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
build/assets/map/map.png
Normal file
After Width: | Height: | Size: 822 KiB |
3
build/assets/menu/company.svg
Normal 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 |
4
build/assets/menu/country.svg
Normal 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 |
3
build/assets/menu/refactor.svg
Normal 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 |
|
@ -1,18 +1,16 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
webpack: (config) => {
|
webpack: (config) => {
|
||||||
|
|
||||||
|
|
||||||
const oneOfs = config.module.rules.find((rule) => !!rule.oneOf).oneOf;
|
const oneOfs = config.module.rules.find((rule) => !!rule.oneOf).oneOf;
|
||||||
for (const oneOf of oneOfs) {
|
for (const oneOf of oneOfs) {
|
||||||
oneOf?.use?.forEach((someUse) => {
|
oneOf?.use?.forEach((someUse) => {
|
||||||
if (!someUse?.options?.modules?.mode) return;
|
if (!someUse?.options?.modules?.mode) return;
|
||||||
// someUse.options.modules.localIdentName = '[local]_';
|
// someUse.options.modules.localIdentName = '[local]';
|
||||||
someUse.options.modules.getLocalIdent = (context, localIdentName, localName, options) => {
|
someUse.options.modules.getLocalIdent = (context, localIdentName, localName) => {
|
||||||
return localName;
|
return localName;
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
BIN
public/assets/map/2x1.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
public/assets/map/map.png
Normal file
After Width: | Height: | Size: 822 KiB |
3
public/assets/menu/company.svg
Normal 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 |
4
public/assets/menu/country.svg
Normal 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 |
3
public/assets/menu/refactor.svg
Normal 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 |
32
src/ts/components/TimeZoneMap/components/Point.tsx
Normal 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;
|
16
src/ts/components/TimeZoneMap/helpers/index.ts
Normal 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'}`;
|
||||||
|
}
|
39
src/ts/components/TimeZoneMap/index.tsx
Normal 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;
|
84
src/ts/components/TimeZoneMap/styles/index.module.scss
Normal 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%; }
|
||||||
|
}
|
|
@ -2,8 +2,6 @@ import ICommit from 'ts/interfaces/Commit';
|
||||||
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
|
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
|
||||||
|
|
||||||
import { ONE_DAY } from 'ts/helpers/formatter';
|
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 { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
|
||||||
|
|
||||||
import userSettings from 'ts/store/UserSettings';
|
import userSettings from 'ts/store/UserSettings';
|
||||||
|
@ -63,9 +61,10 @@ export default class DataGripByAuthor {
|
||||||
statistic.lastCompany = commit.company;
|
statistic.lastCompany = commit.company;
|
||||||
statistic.company.push({ title: commit.company, from: commit.timestamp });
|
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.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);
|
const commitsByHour = new Array(24).fill(0);
|
||||||
commitsByHour[commit.hours] += 1;
|
commitsByHour[commit.hours] += 1;
|
||||||
|
|
||||||
const country = commit.country
|
|
||||||
|| getCountryBySymbol(commit.author)
|
|
||||||
|| getCountryBySymbol(commit.message)
|
|
||||||
|| getCountryByTimeZone(commit.timezone, commit.author);
|
|
||||||
|
|
||||||
this.commits.set(commit.author, {
|
this.commits.set(commit.author, {
|
||||||
author: commit.author,
|
author: commit.author,
|
||||||
commits: 1,
|
commits: 1,
|
||||||
|
@ -95,8 +89,9 @@ export default class DataGripByAuthor {
|
||||||
? [{ title: commit.company, from: commit.milliseconds }]
|
? [{ title: commit.company, from: commit.milliseconds }]
|
||||||
: [],
|
: [],
|
||||||
lastCompany: commit.company,
|
lastCompany: commit.company,
|
||||||
country: new Set([country]),
|
country: [{ country: commit.country, timezone: commit.timezone, from: commit.milliseconds }],
|
||||||
lastCountry: country,
|
lastTimezone: commit.timezone,
|
||||||
|
lastCountry: commit.country,
|
||||||
device: commit.device,
|
device: commit.device,
|
||||||
commitsByDayAndHour,
|
commitsByDayAndHour,
|
||||||
commitsByHour,
|
commitsByHour,
|
||||||
|
|
49
src/ts/helpers/DataGrip/components/country.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
23
src/ts/helpers/FileGrip/components/FileBuilder/Tasks.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
51
src/ts/helpers/FileGrip/components/refactor.ts
Normal 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 = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import IHashMap from 'ts/interfaces/HashMap';
|
||||||
import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
|
import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
|
||||||
import getInfoFromNameAndEmail from './getCompany';
|
import getInfoFromNameAndEmail from './getCompany';
|
||||||
import { getGithubPrInfo } from './getMergeInfo';
|
import { getGithubPrInfo } from './getMergeInfo';
|
||||||
|
import getCountryByTimeZone from './getCountryByTimeZone';
|
||||||
|
|
||||||
const MASTER_BRANCH = {
|
const MASTER_BRANCH = {
|
||||||
master: true,
|
master: true,
|
||||||
|
@ -47,13 +48,17 @@ export default function getCommitInfo(
|
||||||
let email = parts[2] || '';
|
let email = parts[2] || '';
|
||||||
if (email.indexOf('@') === -1) email = '';
|
if (email.indexOf('@') === -1) email = '';
|
||||||
|
|
||||||
const companyKey = `${author}>in>${email}`;
|
const companyKey = `${author}>mail>${email}`;
|
||||||
if (!refEmailAuthor[companyKey]) {
|
if (!refEmailAuthor[companyKey]) { // @ts-ignore
|
||||||
// @ts-ignore
|
|
||||||
refEmailAuthor[companyKey] = getInfoFromNameAndEmail(author, email);
|
refEmailAuthor[companyKey] = getInfoFromNameAndEmail(author, email);
|
||||||
}
|
} // @ts-ignore
|
||||||
// @ts-ignore
|
const { company, domain, device } = refEmailAuthor[companyKey];
|
||||||
const { company, country, 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, '');
|
const authorID = author.replace(/\s|\t/gm, '');
|
||||||
if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) {
|
if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import getCountryByDomain from './getCountryByDomain';
|
|
||||||
import getDevice from './getDevice';
|
import getDevice from './getDevice';
|
||||||
|
|
||||||
const PUBLIC_SERVICES = [
|
const PUBLIC_SERVICES = [
|
||||||
|
@ -22,6 +21,7 @@ const PUBLIC_SERVICES = [
|
||||||
'me',
|
'me',
|
||||||
'qq',
|
'qq',
|
||||||
'dev',
|
'dev',
|
||||||
|
'list',
|
||||||
'localhost',
|
'localhost',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -73,7 +73,6 @@ function isUserName(author?: string, company?: string): boolean {
|
||||||
export default function getInfoFromNameAndEmail(author?: string, email?: string) {
|
export default function getInfoFromNameAndEmail(author?: string, email?: string) {
|
||||||
const companyByAuthor = getCompanyByName(author);
|
const companyByAuthor = getCompanyByName(author);
|
||||||
const [companyByEmail, domain] = getCompanyAndDomainByEmail(email);
|
const [companyByEmail, domain] = getCompanyAndDomainByEmail(email);
|
||||||
const country = getCountryByDomain(domain);
|
|
||||||
const device = getDevice(companyByEmail);
|
const device = getDevice(companyByEmail);
|
||||||
|
|
||||||
const companyName = companyByAuthor || companyByEmail || '';
|
const companyName = companyByAuthor || companyByEmail || '';
|
||||||
|
@ -84,5 +83,5 @@ export default function getInfoFromNameAndEmail(author?: string, email?: string)
|
||||||
|| isIP.test(companyName);
|
|| isIP.test(companyName);
|
||||||
const company = (!isInCorrect && !device) ? companyName : '';
|
const company = (!isInCorrect && !device) ? companyName : '';
|
||||||
|
|
||||||
return { company, country, device };
|
return { company, domain, device };
|
||||||
}
|
}
|
||||||
|
|
253
src/ts/helpers/Parser/getCountryByDomain.ts
Normal 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',
|
||||||
|
};
|
168
src/ts/helpers/Parser/getCountryByTimeZone.ts
Normal 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;
|
||||||
|
}
|
87
src/ts/helpers/Parser/getCountryDistance.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
5
src/ts/helpers/Parser/getDevice.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export default function getDevice(company?: string) {
|
||||||
|
return company && (/(MACBOOK)|(-AIR)|(-IMAC)/gi).test(company)
|
||||||
|
? 'MacBook'
|
||||||
|
: '';
|
||||||
|
}
|
14
src/ts/helpers/Parser/getMergeInfo.ts
Normal 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];
|
||||||
|
}
|
65
src/ts/pages/Team/components/Author/components/PieCharts.tsx
Normal 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;
|
201
src/ts/pages/Team/components/Author/components/View.tsx
Normal 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;
|
15
src/ts/pages/Team/components/Author/contstants.ts
Normal 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'),
|
||||||
|
};
|
78
src/ts/pages/Team/components/Author/index.tsx
Normal 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;
|
63
src/ts/pages/Team/components/Company/components/Charts.tsx
Normal 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;
|
135
src/ts/pages/Team/components/Company/components/Companies.tsx
Normal 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;
|
146
src/ts/pages/Team/components/Company/components/Employments.tsx
Normal 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;
|
62
src/ts/pages/Team/components/Country/components/Charts.tsx
Normal 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;
|
|
@ -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;
|
61
src/ts/pages/Team/components/Country/components/Fly.tsx
Normal 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;
|
87
src/ts/pages/Team/components/Country/components/Travel.tsx
Normal 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;
|
39
src/ts/pages/Team/components/Country/components/VPN.tsx
Normal 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;
|
76
src/ts/pages/Team/components/Country/index.tsx
Normal 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;
|
75
src/ts/pages/Team/components/Files/Tasks.tsx
Normal 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;
|
84
src/ts/pages/Team/components/PR/Anonymous.tsx
Normal 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;
|
167
src/ts/pages/Team/components/Refactor.tsx
Normal 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;
|
||||||
|
|
124
src/ts/pages/Team/components/Release/View.tsx
Normal 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;
|
58
src/ts/pages/Team/components/Release/index.tsx
Normal 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;
|
61
src/ts/pages/Team/components/Release/saveChangeLog.ts
Normal 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');
|
||||||
|
}
|
60
src/ts/pages/Team/components/Tasks/Filters.tsx
Normal 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;
|
69
src/ts/pages/Team/components/Tasks/Release.tsx
Normal 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;
|
126
src/ts/pages/Team/components/Tasks/View.tsx
Normal 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;
|
53
src/ts/pages/Team/components/Tasks/index.tsx
Normal 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;
|
6
src/ts/pages/Team/styles/release.module.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
@import 'src/styles/variables';
|
||||||
|
|
||||||
|
.team_release_download {
|
||||||
|
position: relative;
|
||||||
|
top: -12px;
|
||||||
|
}
|
|
@ -134,8 +134,14 @@ export default `
|
||||||
§ page.team.country.pieByTimezone.title: По времени
|
§ page.team.country.pieByTimezone.title: По времени
|
||||||
§ page.team.country.chart.item: сотрудников
|
§ page.team.country.chart.item: сотрудников
|
||||||
§ page.team.country.table.title: Список сотрудников
|
§ page.team.country.table.title: Список сотрудников
|
||||||
§ page.team.country.table.country: Местоположение
|
§ page.team.country.table.country: Локация
|
||||||
§ page.team.country.table.employments: Сотрудники
|
§ 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.title: Кандидаты на рефакторинг
|
||||||
§ page.team.refactor.lines: строк
|
§ page.team.refactor.lines: строк
|
||||||
§ page.team.refactor.tasks: задач
|
§ page.team.refactor.tasks: задач
|
||||||
|
|