test $ -.&? ${}

This commit is contained in:
bakhirev 2024-10-06 23:47:37 +03:00
parent 6f6ac5a749
commit 81cb8a1b40
54 changed files with 8779 additions and 8116 deletions

View file

@ -1 +1 @@
<!doctype html><html><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="theme-color" content="white"/><meta name="defaultLanguage" content="ru"><meta name="availableLanguages" content="en, es, fr, ja, pt, de, zh, ru"><link rel="canonical" href="https://assayo.online/demo/"><script type="text/javascript">var report=[];function r(r){report.push(r)}var f=String.raw.bind(String)</script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>Git Statistics</title><script src='./log.txt'></script><script src='./log-1.txt'></script><script src='./log-2.txt'></script><script src='./log-3.txt'></script><script src='./log-4.txt'></script><script src='./log-5.txt'></script><script src='./log-6.txt'></script><script src='../log.txt'></script><script src='../log-1.txt'></script><script src='../log-2.txt'></script><script src='../log-3.txt'></script><script src='../log-4.txt'></script><script src='../log-5.txt'></script><script src='../log-6.txt'></script><script src='../../log.txt'></script><script src='../../log-1.txt'></script><script src='../../log-2.txt'></script><script src='../../log-3.txt'></script><script src='../../log-4.txt'></script><script src='../../log-5.txt'></script><script src='../../log-6.txt'></script><script src='/log.txt'></script><script src='/log-1.txt'></script><script src='/log-2.txt'></script><script src='/log-3.txt'></script><script src='/log-4.txt'></script><script src='/log-5.txt'></script><script src='/log-6.txt'></script><meta name="description" content="Simple and fast report on git commit history."><meta name="keywords" content="git, statistics, audit, history, log, monitoring, employee control"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="Git statistics"><meta name="msapplication-tooltip" content="Simple and fast report on Git commit history."><meta property="og:title" content="Git Statistics"><meta property="og:description" content="Simple and fast report on Git commit history."><meta property="og:image" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="https://assayo.online/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Git Statistics"><meta name="twitter:description" content="Simple and fast report on Git commit history."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.online"><meta name="twitter:site" content="assayo.online"><meta itemprop="name" content="Git Statistics"><meta itemprop="description" content="Simple and fast report on Git commit history."><meta itemprop="image" content="https://assayo.online/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/index.js"></script><link href="./static/index.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="theme-color" content="white"/><meta name="defaultLanguage" content="ru"><meta name="availableLanguages" content="en, es, fr, ja, pt, de, zh, ru"><link rel="canonical" href="https://assayo.online/demo/"><script type="text/javascript">var report=[];function r(r){report.push(r)}function R(r){report=report.concat(r.split("\n"))}var f=String.raw.bind(String)</script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><title>Git Statistics</title><script src='./log.txt'></script><script src='./log-1.txt'></script><script src='./log-2.txt'></script><script src='./log-3.txt'></script><script src='./log-4.txt'></script><script src='./log-5.txt'></script><script src='./log-6.txt'></script><script src='../log.txt'></script><script src='../log-1.txt'></script><script src='../log-2.txt'></script><script src='../log-3.txt'></script><script src='../log-4.txt'></script><script src='../log-5.txt'></script><script src='../log-6.txt'></script><script src='../../log.txt'></script><script src='../../log-1.txt'></script><script src='../../log-2.txt'></script><script src='../../log-3.txt'></script><script src='../../log-4.txt'></script><script src='../../log-5.txt'></script><script src='../../log-6.txt'></script><script src='/log.txt'></script><script src='/log-1.txt'></script><script src='/log-2.txt'></script><script src='/log-3.txt'></script><script src='/log-4.txt'></script><script src='/log-5.txt'></script><script src='/log-6.txt'></script><meta name="description" content="Simple and fast report on git commit history."><meta name="keywords" content="git, statistics, audit, history, log, monitoring, employee control"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="Git statistics"><meta name="msapplication-tooltip" content="Simple and fast report on Git commit history."><meta property="og:title" content="Git Statistics"><meta property="og:description" content="Simple and fast report on Git commit history."><meta property="og:image" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="https://assayo.online/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Git Statistics"><meta name="twitter:description" content="Simple and fast report on Git commit history."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="https://assayo.online/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.online"><meta name="twitter:site" content="assayo.online"><meta itemprop="name" content="Git Statistics"><meta itemprop="description" content="Simple and fast report on Git commit history."><meta itemprop="image" content="https://assayo.online/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/index.js"></script><link href="./static/index.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -145,7 +145,7 @@
},
{
"pre": [
"git --no-pager log --raw --numstat --oneline --all --reverse --date=iso-strict --pretty=format:\"%ad>%aN>%aE>%s\" | sed -e 's/\\\\/\\\\\\\\/g' | sed -e 's/`/\"/g' | sed -e 's/^/r(f\\`/g' | sed 's/$/\\`\\);/g' | sed 's/\\$/_/g' > log.txt"
"git --no-pager log --raw --numstat --oneline --all --reverse --date=iso-strict --pretty=format:\"%ad>%aN>%aE>%s\" | sed -e 's/\\\\/\\\\\\\\/g' | sed -e 's/`/\"/g' | sed -e 's/\\$/S/g' | sed -e '1s/^/R(f\\`/' | sed -e '$s/$/\\`\\);/' > log.txt",
]
},
{

View file

@ -18,10 +18,13 @@
<script type="text/javascript">
var report = [];
var f = String.raw.bind(String);
function r(t) {
report.push(t);
}
var f = String.raw.bind(String);
function R(t) {
report = report.concat(t.split("\n"));
}
</script>
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@ import PageWrapper from '../Page/wrapper';
interface IDataViewProps {
rowsForExcel?: any[];
rows: any[];
mode?: string;
type?: string;
sort?: ISort[];
columnCount?: number,
@ -32,6 +33,7 @@ function DataView({
rows = [],
sort = [],
type,
mode,
columnCount,
className,
fullScreenMode = '',
@ -58,6 +60,7 @@ function DataView({
return (
<>
{mode !== 'details' && (
<div style={{ position: 'relative' }}>
<div className={style.data_view_buttons}>
{!isMobile && (
@ -95,8 +98,9 @@ function DataView({
)}
</div>
</div>
)}
{localType === 'table' && (
{localType === 'table' && mode !== 'details' && (
<PageWrapper template="table">
<Table
rows={rows}
@ -109,6 +113,17 @@ function DataView({
</PageWrapper>
)}
{localType === 'table' && mode === 'details' && (
<Table
rows={rows}
sort={sort}
disabledRow={disabledRow}
updateSort={updateSort}
>
{children}
</Table>
)}
{localType === 'cards' && (
<Cards
items={rows}

View file

@ -39,7 +39,7 @@ function CommitInfo({ commits }: { commits: ICommit[] }): React.ReactElement {
function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
const items = Object.entries(tasks)
.map(([task, commits]: [string, any]) => {
const prId = dataGrip.pr.prByTask[task];
const prId = dataGrip.pr.prByTask.get(task);
return (
<>
<div className={style.day_info_link}>

View file

@ -8,28 +8,31 @@
// onChange('meta', { byTaskId });
// }
import splashScreenStore from 'ts/components/SplashScreen/store';
function getGlobalValue() { // @ts-ignore
return window.report;
}
function setGlobalValue(value?: any) { // @ts-ignore
window.report = value || [];
splashScreenStore.setDelay((value || [])?.length);
}
export function getStringsForParser(text: string) {
let temp = getGlobalValue();
setGlobalValue([]);
const firstText = text.slice(0, 3);
if (firstText === 'rep' || firstText === 'r(f') {
try {
eval(text);
} catch (e) {
setGlobalValue(temp);
return;
const isNeedClear = {
'rep': true,
'r(f': true,
'R(f': true,
}[firstText];
if (isNeedClear) {
text = text.replace(/(R\(f`)|(r\(f`)|(report\.push\(`)|(`\);)/gim, '');
}
} else {
setGlobalValue(text.split('\n'));
}
return getGlobalValue();
}
@ -56,7 +59,6 @@ export function getOnDrop(setLoading: Function, onChange: Function) {
.map((file: any) => file.kind === 'file' ? file?.getAsFile() : null)
.filter(file => file);
console.log(files);
setLoading(false);
if (!files.length) return;

View file

@ -17,7 +17,7 @@ function GetItem({ commit, mode }: IGetItemProps) {
const className = size > 5
? style.get_list_big_number
: '';
const prId = dataGrip.pr.prByTask[commit.task];
const prId = dataGrip.pr.prByTask.get(commit.task);
return (
<div className={style.get_list}>

View file

@ -12,16 +12,27 @@ const SplashScreen = observer((): React.ReactElement | null => {
if (!splashScreenStore.isOpen) return;
setTimeout(() => {
splashScreenStore.hide();
}, 5400);
}, splashScreenStore.delay);
}, [splashScreenStore.isOpen]);
if (!splashScreenStore.isOpen) return null;
return (
<div className={style.splash_screen}>
<div className={style.splash_screen_container}>
<div
className={style.splash_screen}
style={{ animationDelay: splashScreenStore.getDelay(100) }}
>
<div
className={style.splash_screen_container}
style={{ animationDelay: splashScreenStore.getDelay(-1400) }}
>
<Logo center />
<div className={progress.progress_bar}></div>
<div className={progress.progress_bar}>
<div
className={progress.progress_bar_line}
style={{ animationDuration: splashScreenStore.getDelay(-1100) }}
/>
</div>
</div>
</div>
);

View file

@ -7,8 +7,7 @@
background-color: #404148;
&:after {
content: '';
&_line {
font-size: 0.8em;
display: block;
width: 0;
@ -33,11 +32,11 @@
}
.progress_bar,
.progress_bar:after {
.progress_bar_line {
transition: background-color 0.7s, width 0.5s;
}
.progress_bar:after {
.progress_bar_line {
animation: progress_bar 4.3s linear forwards;
}

View file

@ -1,25 +1,41 @@
import { observable, action, makeObservable } from 'mobx';
import globalScroll from 'ts/helpers/globalScroll';
const DEFAULT_DELAY = 3400;
class SplashScreenStore {
isOpen: boolean = false;
delay: number = DEFAULT_DELAY;
constructor() {
makeObservable(this, {
isOpen: observable,
delay: observable,
show: action,
hide: action,
setDelay: action,
});
}
show() {
this.isOpen = true;
globalScroll.off(5400);
globalScroll.off(this.delay);
}
hide() {
this.isOpen = false;
}
setDelay(logSize: number) {
const delay = (logSize / 190) + 400;
this.delay = Math.max(DEFAULT_DELAY, delay);
}
getDelay(diff?: number) {
const delay = this.delay + (diff || 0);
return (delay / 1000).toFixed(1) + 's';
}
}
const splashScreen = new SplashScreenStore();

View file

@ -1,6 +1,7 @@
import React from 'react';
import { IColumn, IRowsConfig } from '../../interfaces/Column';
import getClassName from '../../helpers/getClassName';
import style from '../../styles/index.module.scss';
interface IDefaultCellProps {
@ -24,14 +25,12 @@ function DetailsCell({
const left = column?.isFixed ? marginLeft : 0;
const columnClassName = typeof column.className === 'function'
? column.className('body', row)
: column.className;
const iconClassName = config?.details
? style.table_cell_icon_open
: style.table_cell_icon_close;
const localClassName = getClassName(style.table_cell, column, ['body', row], className);
const hasIcon = ((column.properties && row[column.properties])
|| !column.properties
|| !column.properties?.length)
@ -47,13 +46,13 @@ function DetailsCell({
return (
<div
key={column.title} // @ts-ignore
className={`${style.table_cell} ${className || ''} ${columnClassName || ''}`}
key={column.title}
className={localClassName}
style={{
left,
width: column.width,
cursor: 'pointer',
left,
}} // @ts-ignore
}}
onClick={onClick}
>
{hasIcon && (

View file

@ -41,11 +41,13 @@
position: relative;
font-weight: 100;
display: block;
padding-top: 24px;
padding-left: 12px;
padding: 0 var(--space-m) var(--space-xxl) 42px;
white-space: nowrap;
border-bottom: 1px solid #EEEEEE;
break-inside: auto;
border-bottom: 1px solid #EEEEEE;
border-radius: var(--border-radius-m);
}
&_cell,
@ -62,6 +64,8 @@
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
background-color: var(--color-white);
}
&_fixed {
@ -97,11 +101,11 @@
margin-top: 8px;
cursor: pointer;
transition: transform 0.5s;
transform: rotate(0);
transform: rotate(-90deg);
}
&_open {
transform: rotate(-180deg);
transform: rotate(0);
}
}
}

View file

@ -43,7 +43,7 @@ interface ITaskProps {
function Task({ title, commits }: ITaskProps) {
const { t } = useTranslation();
const prId = dataGrip.pr.prByTask[title];
const prId = dataGrip.pr.prByTask.get(title);
return (
<div
key={title}

View file

@ -1,16 +1,16 @@
import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { ONE_DAY } from 'ts/helpers/formatter';
import { increment } from 'ts/helpers/Math';
import getCompany from '../helpers/getCompany';
import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
import getCompany from 'ts/helpers/Parser/getCompany';
import userSettings from 'ts/store/UserSettings';
export default class DataGripByAuthor {
list: string[] = [];
commits: IHashMap<any> = {};
commits: HashMap<any> = new Map();
statistic: any = [];
@ -20,22 +20,22 @@ export default class DataGripByAuthor {
clear() {
this.list = [];
this.commits = {};
this.commits.clear();
this.statistic = [];
this.statisticByName = {};
}
addCommit(commit: ICommit) {
if (this.commits.hasOwnProperty(commit.author)) {
this.#updateCommitByAuthor(commit);
const statistic = this.commits.get(commit.author);
if (statistic) {
this.#updateCommitByAuthor(statistic, commit);
} else {
this.#addCommitByAuthor(commit);
}
this.#setMoneyByMonth(commit);
}
#updateCommitByAuthor(commit: ICommit) {
const statistic = this.commits[commit.author];
#updateCommitByAuthor(statistic: any, commit: ICommit) {
statistic.commits += 1;
statistic.lastCommit = commit;
statistic.days[commit.timestamp] = true;
@ -56,6 +56,11 @@ export default class DataGripByAuthor {
}
statistic.commitsByHour[commit.hours] += 1;
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
if (commit.company && statistic.lastCompany !== commit.company) {
statistic.lastCompany = commit.company;
statistic.company.push({ title: commit.company, from: commit.timestamp });
}
}
#addCommitByAuthor(commit: ICommit) {
@ -65,16 +70,20 @@ export default class DataGripByAuthor {
const commitsByHour = new Array(24).fill(0);
commitsByHour[commit.hours] += 1;
this.commits[commit.author] = {
this.commits.set(commit.author, {
author: commit.author,
commits: 1,
firstCommit: commit,
lastCommit: commit,
days: { [commit.timestamp]: true },
days: createHashMap(commit.timestamp),
tasks: { [commit.task]: commit.added + commit.changes + commit.removed },
types: { [commit.type]: 1 },
scopes: { [commit.scope]: 1 },
types: createIncrement(commit.type),
scopes: createIncrement(commit.scope),
hours: [commit.hours],
company: commit.company
? [{ title: commit.company, from: commit.timestamp }]
: [],
lastCompany: commit.company,
commitsByDayAndHour,
commitsByHour,
messageLength: [commit.text.length || 0],
@ -82,12 +91,12 @@ export default class DataGripByAuthor {
maxMessageLength: commit.text.length || 0,
wordStatistics: DataGripByAuthor.#updateWordStatistics(commit),
moneyByMonth: {},
};
});
}
#setMoneyByMonth(commit: ICommit) {
const key = `${commit.year}-${commit.month}`;
if (this.commits[commit.author].moneyByMonth[key]) {
if (this.commits.get(commit.author).moneyByMonth[key]) {
this.#updateMoneyByMonth(commit, key);
} else {
this.#addMoneyByMonth(commit, key);
@ -95,7 +104,7 @@ export default class DataGripByAuthor {
}
#updateMoneyByMonth(commit: ICommit, key: string) {
const statistic = this.commits[commit.author].moneyByMonth[key];
const statistic = this.commits.get(commit.author).moneyByMonth[key];
if (statistic.alreadyAdded[commit.milliseconds]) return;
statistic.alreadyAdded[commit.milliseconds] = true;
@ -110,7 +119,7 @@ export default class DataGripByAuthor {
#addMoneyByMonth(commit: ICommit, key: string) {
const contract = userSettings.getEmploymentContract(commit.author, commit.milliseconds);
const isWorkDay = contract.workDaysInWeek[commit.day];
this.commits[commit.author].moneyByMonth[key] = {
this.commits.get(commit.author).moneyByMonth[key] = {
workDay: isWorkDay ? 1 : 0,
weekDay: isWorkDay ? 0 : 1,
alreadyAdded: {
@ -147,7 +156,7 @@ export default class DataGripByAuthor {
active: [],
};
this.statistic = Object.values(this.commits)
this.statistic = Array.from(this.commits.values())
.sort((dotA: any, dotB: any) => dotB.commits - dotA.commits)
.map((dot: any) => {
const from = dot.firstCommit.milliseconds;
@ -186,7 +195,7 @@ export default class DataGripByAuthor {
daysForTask: isStaff ? 0 : workDays / tasks.length,
taskInDay: isStaff ? 0 : tasks.length / workDays,
changesForTask: DataGripByAuthor.getMiddleValue(tasksSize),
company: getCompany(dot.author, dot.lastCommit.email),
lastCompany: getCompany(dot.author, dot.lastCommit.email),
days: workDays,
money: isStaff ? 0 : moneyWorked,

View file

@ -0,0 +1,87 @@
import ICommit from 'ts/interfaces/Commit';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { createIncrement, increment } from 'ts/helpers/Math';
export default class DataGripByCompany {
commits: HashMap<any> = new Map();
statistic: any = [];
statisticByName: IHashMap<any> = {};
clear() {
this.commits.clear();
this.statistic = [];
this.statisticByName = {};
}
addCommit(commit: ICommit) {
if (!commit.company) return;
const statistic = this.commits.get(commit.company);
if (statistic) {
this.#updateCommitByCompany(statistic, commit);
} else {
this.#addCommitByCompany(commit);
}
}
#addCommitByCompany(commit: ICommit) {
this.commits.set(commit.company, {
company: commit.company,
commits: 1,
firstCommit: commit,
lastCommit: commit,
days: createIncrement(commit.timestamp),
employments: createIncrement(commit.author),
tasks: createIncrement(commit.task),
types: createIncrement(commit.type),
scopes: createIncrement(commit.scope),
});
}
#updateCommitByCompany(statistic: any, commit: ICommit) {
statistic.commits += 1;
statistic.lastCommit = commit;
statistic.days[commit.timestamp] = true;
statistic.employments[commit.author] = true;
increment(statistic.tasks, commit.task);
increment(statistic.types, commit.type);
increment(statistic.scopes, commit.scope);
}
updateTotalInfo(dataGripByAuthor: any) {
console.dir(dataGripByAuthor);
this.statistic = Array.from(this.commits.values())
.sort((dotA: any, dotB: any) => dotB.commits - dotA.commits)
.map((statistic: any) => {
const tasks = Object.keys(statistic.tasks);
const days = Object.keys(statistic.days);
const employments = Object.keys(statistic.employments);
let isActive = false;
employments.forEach((name) => {
const author = dataGripByAuthor.statisticByName[name];
if (!author) return;
if (author.lastCompany === statistic.company) isActive = true;
});
const companyInfo = {
...statistic,
employments,
tasks,
totalTasks: tasks.length,
totalDays: days.length,
totalEmployments: employments.length,
isActive,
};
delete companyInfo.days;
this.statisticByName[statistic.company] = companyInfo;
return companyInfo;
});
this.commits.clear();
}
}

View file

@ -1,6 +1,6 @@
import { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import { increment, WeightedAverage } from 'ts/helpers/Math';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { createIncrement, increment, WeightedAverage } from 'ts/helpers/Math';
const IS_PR = {
[COMMIT_TYPE.PR_BITBUCKET]: true,
@ -9,71 +9,69 @@ const IS_PR = {
};
export default class DataGripByPR {
pr: IHashMap<any> = {};
pr: HashMap<any> = new Map();
prByTask: IHashMap<string> = {};
prByTask: HashMap<any> = new Map();
lastCommitByTaskNumber: IHashMap<any> = {};
lastCommitByTaskNumber: HashMap<any> = new Map();
statistic: any[] = [];
statisticByName: IHashMap<any> = [];
clear() {
this.pr = {};
this.prByTask = {};
this.lastCommitByTaskNumber = {};
this.pr.clear();
this.prByTask.clear();
this.lastCommitByTaskNumber.clear();
this.statistic = [];
}
addCommit(commit: ISystemCommit) {
if (!commit.commitType) {
if (!this.lastCommitByTaskNumber[commit.task]) {
this.#addCommitByTaskNumber(commit);
const commitByTaskNumber = this.lastCommitByTaskNumber.get(commit.task);
if (commitByTaskNumber) {
this.#updateCommitByTaskNumber(commitByTaskNumber, commit);
} else {
this.#updateCommitByTaskNumber(commit);
this.#addCommitByTaskNumber(commit);
}
} else if (!this.pr[commit.prId] && IS_PR[commit.commitType || '']) {
} else if (!this.pr.has(commit.prId) && IS_PR[commit.commitType || '']) {
this.#addCommitByPR(commit);
}
}
#addCommitByTaskNumber(commit: ISystemCommit) {
this.lastCommitByTaskNumber[commit.task] = {
this.lastCommitByTaskNumber.set(commit.task, {
commits : 1,
beginTaskTime: commit.milliseconds,
endTaskTime: commit.milliseconds,
commitsByAuthors: {
[commit.author]: 1,
},
commitsByAuthors: createIncrement(commit.author),
firstCommit: commit,
};
});
}
#updateCommitByTaskNumber(commit: ISystemCommit) {
const statistic = this.lastCommitByTaskNumber[commit.task];
#updateCommitByTaskNumber(statistic: any, commit: ISystemCommit) {
statistic.endTaskTime = commit.milliseconds;
statistic.commits += 1;
increment(statistic.commitsByAuthors, commit.author);
}
#addCommitByPR(commit: ISystemCommit) {
const lastCommit = this.lastCommitByTaskNumber[commit.task];
const lastCommit = this.lastCommitByTaskNumber.get(commit.task);
if (lastCommit) {
// коммиты после влития PR сгорают, чтобы не засчитать технические PR мержи веток
delete this.lastCommitByTaskNumber[commit.task];
this.lastCommitByTaskNumber.delete(commit.task);
const delay = commit.milliseconds - lastCommit.endTaskTime;
const work = lastCommit.endTaskTime - lastCommit.beginTaskTime;
this.pr[commit.prId] = {
this.pr.set(commit.prId, {
...commit,
...lastCommit,
delay,
delayDays: delay / (24 * 60 * 60 * 1000),
workDays: work === 0 ? 1 : (work / (24 * 60 * 60 * 1000)),
};
this.prByTask[commit.task] = commit.prId;
});
this.prByTask.set(commit.task, commit.prId);
} else {
this.pr[commit.prId] = { ...commit };
this.pr.set(commit.prId, { ...commit });
}
}
@ -101,7 +99,7 @@ export default class DataGripByPR {
this.statistic.sort((a: any, b: any) => b.delay - a.delay);
this.updateTotalByAuthor(authors, refAuthorPR);
this.lastCommitByTaskNumber = {};
this.lastCommitByTaskNumber.clear();
}
static getPRByGroups(list: any, propertyName: string) {

View file

@ -1,7 +1,7 @@
import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import userSettings from 'ts/store/UserSettings';
import { increment } from 'ts/helpers/Math';
import { createHashMap, createIncrement, increment } from 'ts/helpers/Math';
interface IStatByAuthor {
commits: number; // number of commits by author in this scope
@ -60,10 +60,10 @@ export default class DataGripByScope {
this.commits[commit.scope] = {
scope: commit.scope,
commits: 1,
days: { [commit.timestamp]: true },
tasks: { [commit.task]: true },
types: { [commit.type]: 1 },
authors: { [commit.author]: this.#getDefaultAuthorForScope(commit) },
days: createHashMap(commit.timestamp),
tasks: createHashMap(commit.task),
types: createIncrement(commit.type),
authors: createIncrement(commit.author, this.#getDefaultAuthorForScope(commit)),
};
}

View file

@ -40,7 +40,7 @@ export default class DataGripByTasks {
const firstCommit = commits[0];
const lastCommit = commits[commits.length - 1];
const from = firstCommit.milliseconds;
const pr = PRs.prByTask[task] ? PRs.pr[PRs.prByTask[task]] : null;
const pr = PRs.prByTask.get(task) ? PRs.pr.get(PRs.prByTask.get(task)) : null;
const shortInfo = {
task,

View file

@ -1,14 +1,14 @@
import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import { HashMap } from 'ts/interfaces/HashMap';
import userSettings from 'ts/store/UserSettings';
import { increment } from 'ts/helpers/Math';
import MinMaxCounter from './counter';
export default class DataGripByTimestamp {
commits: IHashMap<any> = {};
commits: HashMap<any> = new Map();
commitsByAuthor: IHashMap<any> = {};
commitsByAuthor: HashMap<any> = new Map();
statistic: any = [];
@ -19,29 +19,35 @@ export default class DataGripByTimestamp {
}
clear() {
this.commits = {};
this.commitsByAuthor = {};
this.commits.clear();
this.commitsByAuthor.clear();
this.statistic = [];
this.statisticByAuthor = {};
}
addCommit(commit: ICommit) {
if (this.commits[commit.milliseconds]) {
this.#updateCommitByTimestamp(commit, this.commits[commit.milliseconds]);
const commitByMilliseconds = this.commits.get(commit.milliseconds);
if (commitByMilliseconds) {
this.#updateCommitByTimestamp(commitByMilliseconds, commit);
} else {
this.commits[commit.milliseconds] = this.#getDefaultCommitByTimestamp(commit);
this.commits.set(commit.milliseconds, this.#getDefaultCommitByTimestamp(commit));
}
if (!this.commitsByAuthor[commit.author]) {
this.commitsByAuthor[commit.author] = {};
let commitsByAuthor = this.commitsByAuthor.get(commit.author);
if (!commitsByAuthor) {
commitsByAuthor = new Map();
this.commitsByAuthor.set(commit.author, commitsByAuthor);
}
if (this.commitsByAuthor[commit.author][commit.milliseconds]) {
this.#updateCommitByTimestamp(commit, this.commitsByAuthor[commit.author][commit.milliseconds]);
const commitByAuthorMilliseconds = commitsByAuthor.get(commit.milliseconds);
if (commitByAuthorMilliseconds) {
this.#updateCommitByTimestamp(commitByAuthorMilliseconds, commit);
} else {
this.commitsByAuthor[commit.author][commit.milliseconds] = this.#getDefaultCommitByTimestamp(commit);
commitsByAuthor.set(commit.milliseconds, this.#getDefaultCommitByTimestamp(commit));
}
}
#updateCommitByTimestamp(commit: ICommit, statistic: any) {
#updateCommitByTimestamp(statistic: any, commit: ICommit) {
statistic.commits += 1;
statistic.addedAndChanges += commit.added + commit.changes;
increment(statistic.tasks, commit.task);
@ -76,16 +82,16 @@ export default class DataGripByTimestamp {
updateTotalInfo(dataGripByAuthor: any) {
this.statistic = this.#getTotalInfo(this.commits);
this.statistic.weekendPayment = 0;
for (let author in this.commitsByAuthor) {
const statistic = this.#getTotalInfo(this.commitsByAuthor[author]);
statistic.weekendPayment = this.#getWeekendPaymentByAuthor(statistic, dataGripByAuthor.statisticByName[author]);
this.statisticByAuthor[author] = statistic; // TODO: странный результат, неверный расчёт?
for (let author of this.commitsByAuthor.keys()) {
const statistic = this.#getTotalInfo(this.commitsByAuthor.get(author));
statistic.weekendPayment = this.#getWeekendPaymentByAuthor(statistic, dataGripByAuthor.statisticByName[author || '']);
this.statisticByAuthor[author || ''] = statistic; // TODO: странный результат, неверный расчёт?
this.statistic.weekendPayment += statistic.weekendPayment;
}
}
#getTotalInfo(uniqCommitsByTimestamp: any) {
const allCommitsByTimestamp = Object.values(uniqCommitsByTimestamp);
#getTotalInfo(uniqCommitsByTimestamp: HashMap<any>) {
const allCommitsByTimestamp = Array.from(uniqCommitsByTimestamp.values());
const commitsCounter = new MinMaxCounter();
const changesCounter = new MinMaxCounter();
@ -111,12 +117,6 @@ export default class DataGripByTimestamp {
};
}
#getMiddleValue(list: any, property: string) {
const sortList = list.sort((a: any, b: any) => b[property] - a[property]);
const gap = Math.floor(sortList.length * 0.05);
return sortList.slice(gap, sortList.length - gap);
}
#getWeekendPaymentByAuthor(statistic: any, dataGripByAuthor: any) {
if (dataGripByAuthor.isStaff) return 0;
const salaryInMonth = userSettings.getCurrentSalaryInMonth(dataGripByAuthor.author);

View file

@ -1,6 +1,6 @@
import ICommit from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import { increment } from 'ts/helpers/Math';
import { createIncrement, increment } from 'ts/helpers/Math';
import { POPULAR_TYPES } from 'ts/helpers/Parser/getTypeAndScope';
export default class DataGripByType {
@ -39,10 +39,12 @@ export default class DataGripByType {
this.commits[commit.type] = {
type: commit.type,
commits: 1,
days: { [commit.timestamp]: true },
tasks: { [commit.task]: true },
commitsByAuthors: { [commit.author]: 1 },
daysByAuthors: { [commit.author]: { [commit.timestamp]: true } },
days: createIncrement(commit.timestamp, true),
tasks: createIncrement(commit.task, true),
commitsByAuthors: createIncrement(commit.author, true),
daysByAuthors: {
[commit.author]: createIncrement(commit.timestamp, true),
},
};
}

View file

@ -14,12 +14,15 @@ import DataGripByPR from './components/pr';
import DataGripByTasks from './components/tasks';
import DataGripByRelease from './components/release';
import DataGripByScoring from './components/scoring';
import DataGripByCompany from './components/company';
class DataGrip {
firstLastCommit: any = new MinMaxCounter();
author: any = new DataGripByAuthor();
company: any = new DataGripByCompany();
team: any = new DataGripByTeam();
scope: any = new DataGripByScope();
@ -45,6 +48,7 @@ class DataGrip {
clear() {
this.firstLastCommit.clear();
this.author.clear();
this.company.clear();
this.team.clear();
this.scope.clear();
this.type.clear();
@ -71,6 +75,7 @@ class DataGrip {
this.get.addCommit(commit);
this.week.addCommit(commit);
this.tasks.addCommit(commit);
this.company.addCommit(commit);
}
}
@ -86,6 +91,7 @@ class DataGrip {
this.tasks.updateTotalInfo(this.pr);
this.release.updateTotalInfo();
this.scoring.updateTotalInfo(this.author, this.timestamp);
this.company.updateTotalInfo(this.author);
}
}

View file

@ -1,6 +1,7 @@
import ICommit, { IFileChange } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo';
import { increment } from 'ts/helpers/Math';
import FileBuilderCommon from './Common';
import FileBuilderLineStat from './LineStat';
@ -8,25 +9,26 @@ import FileBuilderLineStat from './LineStat';
export default class FileGripByPaths {
list: IDirtyFile[] = [];
refFileIds: IHashMap<IDirtyFile> = {};
refFileIds: HashMap<IDirtyFile> = new Map();
refRemovedFileIds: IHashMap<IDirtyFile> = {};
refRemovedFileIds: HashMap<IDirtyFile> = new Map();
refExtensionType: IHashMap<IHashMap<number>> = {}; // TODO: remove me?
refExtensionType: HashMap<IHashMap<number>> = new Map(); // TODO: remove me?
clear() {
this.list = [];
this.refFileIds = {};
this.refRemovedFileIds = {};
this.refFileIds.clear();
this.refRemovedFileIds.clear();
this.refExtensionType.clear();
}
addCommit(fileChange: IFileChange, commit: ICommit) {
let file = this.refFileIds[fileChange.id] || this.refFileIds[fileChange.newId || ''];
let file = this.refFileIds.get(fileChange.id) || this.refFileIds.get(fileChange.newId);
if (file) {
this.#updateDirtyFile(file, fileChange, commit);
} else {
file = this.#getNewDirtyFile(fileChange, commit) as IDirtyFile;
this.refFileIds[fileChange.id] = file;
this.refFileIds.set(fileChange.id, file);
}
if (fileChange.newId) {
@ -51,20 +53,22 @@ export default class FileGripByPaths {
}
#renameFile(file: any, newId: string) {
this.refFileIds[newId] = this.refFileIds[file.id];
delete this.refFileIds[file.id];
const oldFile = this.refFileIds.get(file.id) as IDirtyFile;
this.refFileIds.set(newId, oldFile);
this.refFileIds.delete(file.id);
file.id = newId;
}
#removeFile(file: any) {
file.action = 'D';
this.refRemovedFileIds[file.id] = this.refFileIds[file.id];
this.refRemovedFileIds[file.id].action = 'D';
delete this.refFileIds[file.id];
const oldFile = this.refFileIds.get(file.id) as IDirtyFile;
oldFile.action = 'D';
this.refRemovedFileIds.set(file.id, oldFile);
this.refFileIds.delete(file.id);
}
updateTotalInfo(callback?: Function) {
this.list = Object.values(this.refFileIds);
this.list = Array.from(this.refFileIds.values());
this.list.forEach((temp: any) => {
const file = temp;
@ -72,10 +76,12 @@ export default class FileGripByPaths {
FileBuilderLineStat.updateTotal(file);
if (file.type) {
if (!this.refExtensionType[file.extension]) this.refExtensionType[file.extension] = {};
this.refExtensionType[file.extension][file.type] = this.refExtensionType[file.extension][file.type]
? (this.refExtensionType[file.extension][file.type] + 1)
: 1;
let refExtensionType = this.refExtensionType.get(file.extension);
if (!refExtensionType) {
refExtensionType = {};
this.refExtensionType.set(file.extension, refExtensionType);
}
increment(refExtensionType, file.type);
}
if (file.lines === 0

View file

@ -1,4 +1,4 @@
import IHashMap from 'ts/interfaces/HashMap';
import { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo';
interface IStatByAuthor {
@ -8,12 +8,12 @@ interface IStatByAuthor {
}
export default class FileGripByAuthor {
statisticByName: IHashMap<IStatByAuthor> = {};
statisticByName: HashMap<IStatByAuthor> = new Map();
totalAddedFiles: number = 0;
clear() {
this.statisticByName = {};
this.statisticByName.clear();
}
addFile(file: IDirtyFile) {
@ -28,17 +28,17 @@ export default class FileGripByAuthor {
}
#addCommitByAuthor(author: string) {
if (this.statisticByName[author]) return;
this.statisticByName[author] = {
if (this.statisticByName.has(author)) return;
this.statisticByName.set(author, {
addedFiles: 0,
removedFiles: 0,
addedWithoutRemoveFiles: 0,
};
});
}
#updateCommitByAuthor(file: IDirtyFile, firstAuthor: string, lastAuthor: string) {
const createStatistic = this.statisticByName[firstAuthor];
const removeStatistic = this.statisticByName[lastAuthor];
const createStatistic = this.statisticByName.get(firstAuthor) as IStatByAuthor;
const removeStatistic = this.statisticByName.get(lastAuthor) as IStatByAuthor;
createStatistic.addedWithoutRemoveFiles += 1;
if (file.action === 'D') {
@ -49,7 +49,7 @@ export default class FileGripByAuthor {
}
updateTotalInfo() {
this.totalAddedFiles = Object.values(this.statisticByName)
this.totalAddedFiles = Array.from(this.statisticByName.values())
.reduce((sum: number, stat: any) => sum + stat.addedFiles, 0);
}
}

View file

@ -1,4 +1,4 @@
import IHashMap from 'ts/interfaces/HashMap';
import { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo';
interface IStatByExtension {
@ -22,7 +22,7 @@ const IGNORE_LIST = [
export default class FileGripByExtension {
statistic: IStatByExtension[] = [];
statisticByName: IHashMap<IStatByExtension> = {};
statisticByName: HashMap<IStatByExtension> = new Map();
property: string = '';
@ -32,7 +32,7 @@ export default class FileGripByExtension {
clear() {
this.statistic = [];
this.statisticByName = {};
this.statisticByName.clear();
}
addFile(file: IDirtyFile) {
@ -40,17 +40,18 @@ export default class FileGripByExtension {
if (!key || IGNORE_LIST.includes(file.name)) return;
if (!this.statisticByName[key]) {
this.statisticByName[key] = this.#getNewExtension(file);
let extension = this.statisticByName.get(key);
if (!extension) {
extension = this.#getNewExtension(file);
this.statisticByName.set(key, extension);
}
const extensions = this.statisticByName[key];
if (file.action === 'D') {
extensions.removedFiles.push(file);
extensions.removedCount += 1;
extension.removedFiles.push(file);
extension.removedCount += 1;
} else {
extensions.files.push(file);
extensions.count += 1;
extension.files.push(file);
extension.count += 1;
}
}
@ -67,8 +68,7 @@ export default class FileGripByExtension {
}
updateTotalInfo() {
this.statistic = Object.entries(this.statisticByName)
.sort((a: any, b: any) => b[1].count - a[1].count)
.map((item: any) => item[1]);
this.statistic = Array.from(this.statisticByName.values())
.sort((a: any, b: any) => b.count - a.count);
}
}

View file

@ -30,6 +30,16 @@ function getFolder(name?: string, path?: string[], file?: IDirtyFile): IFolder {
};
}
function updateFolderBy(folder: any, file: IDirtyFile, property: string) {
for (let author in file[property]) {
const folderAddedLinesByAuthor = folder[property][author];
const fileAddedLinesByAuthor = file[property][author];
folder[property][author] = folderAddedLinesByAuthor
? (folderAddedLinesByAuthor + fileAddedLinesByAuthor)
: fileAddedLinesByAuthor;
}
}
function updateFolder(folder: any, file: IDirtyFile) {
folder.lastCommit = file.lastCommit;
folder.lines += file.lines;
@ -38,23 +48,9 @@ function updateFolder(folder: any, file: IDirtyFile) {
folder.removedLines += file.removedLines || 0;
folder.changedLines += file.changedLines || 0;
for (let author in file.addedLinesByAuthor) {
folder.addedLinesByAuthor[author] = folder.addedLinesByAuthor[author]
? (folder.addedLinesByAuthor[author] + file.addedLinesByAuthor[author])
: file.addedLinesByAuthor[author];
}
for (let author in file.removedLinesByAuthor) {
folder.removedLinesByAuthor[author] = folder.removedLinesByAuthor[author]
? (folder.removedLinesByAuthor[author] + file.removedLinesByAuthor[author])
: file.removedLinesByAuthor[author];
}
for (let author in file.changedLinesByAuthor) {
folder.changedLinesByAuthor[author] = folder.changedLinesByAuthor[author]
? (folder.changedLinesByAuthor[author] + file.changedLinesByAuthor[author])
: file.changedLinesByAuthor[author];
}
updateFolderBy(folder, file, 'addedLinesByAuthor');
updateFolderBy(folder, file, 'removedLinesByAuthor');
updateFolderBy(folder, file, 'changedLinesByAuthor');
}
export default class FileGripByFolder {

View file

@ -1,5 +1,6 @@
import IHashMap from 'ts/interfaces/HashMap';
import IHashMap, { HashMap } from 'ts/interfaces/HashMap';
import { IDirtyFile } from 'ts/interfaces/FileInfo';
import { increment } from 'ts/helpers/Math';
interface IStatByType {
type: string; // type name
@ -15,11 +16,11 @@ interface IStatByType {
export default class FileGripByType {
statistic: IStatByType[] = [];
statisticByName: IHashMap<IStatByType> = {};
statisticByName: HashMap<IStatByType> = new Map();
clear() {
this.statistic = [];
this.statisticByName = {};
this.statisticByName.clear();
}
addFile(file: IDirtyFile) {
@ -27,14 +28,13 @@ export default class FileGripByType {
if (!key || file?.name?.[0] === '.') return;
if (!this.statisticByName.hasOwnProperty(key)) {
this.statisticByName[key] = this.#getNewType(file);
let type = this.statisticByName.get(key);
if (!type) {
type = this.#getNewType(file);
this.statisticByName.set(key, type);
}
const type = this.statisticByName[key];
type.extension[file?.extension] = type.extension[file?.extension]
? (type.extension[file?.extension] + 1)
: 1;
increment(type.extension, file?.extension);
if (file.action === 'D') {
type.removedFiles.push(file);
@ -50,7 +50,7 @@ export default class FileGripByType {
type: file?.type,
task: file?.firstCommit?.task,
path: file?.name,
extension: { [file?.extension]: 1 },
extension: {},
files: [],
count: 0,
removedFiles: [],
@ -59,8 +59,7 @@ export default class FileGripByType {
}
updateTotalInfo() {
this.statistic = Object.entries(this.statisticByName)
.sort((a: any, b: any) => b[1].count - a[1].count)
.map((item: any) => item[1]);
this.statistic = Array.from(this.statisticByName.values())
.sort((a: any, b: any) => b.count - a.count);
}
}

View file

@ -37,5 +37,13 @@ export class WeightedAverage {
}
export function increment(object: Object, path: string) {
object[path] = (object[path] || 0) + 1;
if (path) object[path] = (object[path] || 0) + 1;
}
export function createIncrement(key?: string, firstValue?: any) {
return key ? { [key]: firstValue || 1 } : {};
}
export function createHashMap(key?: string) {
return createIncrement(key, true);
}

View file

@ -2,6 +2,7 @@ import ICommit, { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
import { getTypeAndScope, getTask, getTaskNumber } from './getTypeAndScope';
import getCompany from './getCompany';
const MASTER_BRANCH = {
master: true,
@ -11,11 +12,11 @@ const MASTER_BRANCH = {
let prevDate = new Date();
let refTimestampTime = {};
let refTimestampTime = new Map();
export function clearCache() {
prevDate = new Date();
refTimestampTime = {};
refTimestampTime.clear();
}
export default function getCommitInfo(
@ -28,33 +29,44 @@ export default function getCommitInfo(
const sourceDate = parts[0] || '';
let date = new Date(sourceDate);
if (isNaN(date.getDay())) {
console.log(`PARSE ERROR: Date parse error for: "${logString}"`);
// console.log(`PARSE ERROR: Date parse error for: "${logString}"`);
date = prevDate;
}
prevDate = date;
const day = date.getDay() - 1;
const timestamp = sourceDate.substring(0, 10); // split('T')[0];
if (!refTimestampTime[timestamp]) {
refTimestampTime[timestamp] = (new Date(timestamp)).getTime();
let milliseconds = refTimestampTime.get(timestamp);
if (!milliseconds) {
milliseconds = (new Date(timestamp)).getTime();
refTimestampTime.set(timestamp, milliseconds);
}
let author = parts[1]?.replace(/[._]/gm, ' ') || '';
let email = parts[2] || '';
if (email.indexOf('@') === -1) email = '';
const companyKey = `${author}>in>${email}`;
if (!refEmailAuthor[companyKey]) {
const companyForKey = getCompany(author, email);
// @ts-ignore
refEmailAuthor[companyKey] = { company: companyForKey };
}
// @ts-ignore
const company = refEmailAuthor[companyKey].company;
const authorID = author.replace(/\s|\t/gm, '');
if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) {
console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[authorID]}"`);
// console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[authorID]}"`);
author = refEmailAuthor[authorID];
}
if (email && refEmailAuthor[email] && refEmailAuthor[email] !== author) {
console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[email]}" by "${email}"`);
// console.log(`PARSE WARNING: Rename "${author}" to "${refEmailAuthor[email]}" by "${email}"`);
author = refEmailAuthor[email];
}
if (author && refEmailAuthor[author] && refEmailAuthor[author] !== email) {
console.log(`PARSE WARNING: Rename "${email}" to "${refEmailAuthor[author]}" by "${author}"`);
// console.log(`PARSE WARNING: Rename "${email}" to "${refEmailAuthor[author]}" by "${author}"`);
email = refEmailAuthor[author];
}
@ -75,11 +87,12 @@ export default function getCommitInfo(
year: date.getUTCFullYear(),
week: 0,
timestamp,
milliseconds: refTimestampTime[timestamp],
milliseconds,
author,
email,
message,
company,
text: '',
type: '—',

View file

@ -14,6 +14,11 @@ const PUBLIC_SERVICES = [
'rambler',
'github',
'gitlab',
'com',
'me',
'qq',
'dev',
'localhost',
];
const isPublicService = Object.fromEntries(
@ -36,14 +41,32 @@ function getCompanyByName(author?: string): string {
function getCompanyByEmail(email?: string) {
const domain = (email || '').split('@').pop() || '';
const company = domain.split('.').shift() || '';
return company.toUpperCase();
const parts = domain.split('.');
parts.pop();
return (parts.pop() || '').toUpperCase();
}
function getClearText(text: string) {
return (text || '')
.replace(/(\[[^\]]])+/gim, '')
.replace(/[\s\t._0-9]+/gim, '')
.toLowerCase();
}
function isUserName(author?: string, company?: string): boolean {
if (!author || !company) return false;
const clearAuthor = getClearText(author);
const clearCompany = getClearText(company);
if (!clearAuthor || !clearCompany) return false;
return !!clearAuthor.match(clearCompany);
}
function getCompany(author?: string, email?: string) {
const company = getCompanyByName(author) || getCompanyByEmail(email) || '';
const isMailService = company.indexOf('MAIL') !== -1;
return isPublicService[company] || isMailService
return isPublicService[company] || isMailService || isUserName(author, company)
? ''
: company;
}

View file

@ -5,6 +5,8 @@ function getFilePath(path: string): string[] {
.replace(/"/gm, '')
.replace(/\/\//gm, '/');
if (formattedPath.indexOf('{') === -1) return [formattedPath];
const parts = formattedPath.split(/(?:\{)|(?:\s=>\s)|(?:})/gm);
if (parts.length !== 2 && parts.length !== 4) return [formattedPath];
@ -19,9 +21,32 @@ function getFilePath(path: string): string[] {
return [oldPath, newPath];
}
function fastNumStatSplit(message: string) {
let firstIndex = 0;
if (message[1] === '\t') firstIndex = 1;
else if (message[2] === '\t') firstIndex = 2;
else if (message[3] === '\t') firstIndex = 3;
else if (message[4] === '\t') firstIndex = 4;
else if (message[5] === '\t') firstIndex = 5;
let secondIndex = firstIndex + 2;
if (message[firstIndex + 2] === '\t') secondIndex = firstIndex + 2;
else if (message[firstIndex + 3] === '\t') secondIndex = firstIndex + 3;
else if (message[firstIndex + 4] === '\t') secondIndex = firstIndex + 4;
else if (message[firstIndex + 5] === '\t') secondIndex = firstIndex + 5;
else if (message[firstIndex + 6] === '\t') secondIndex = firstIndex + 6;
return [
message.substring(0, firstIndex),
message.substring(firstIndex + 1, secondIndex),
message.substring(secondIndex + 1),
];
}
// "38 9 src/app.css" -> [38, 9, 'src/app.css']
export function getNumStatInfo(message: string) {
let [addedRaw, removedRaw, path] = message.split('\t');
let [addedRaw, removedRaw, path] = fastNumStatSplit(message);
// let [addedRaw, removedRaw, path] = message.split('\t');
let added = parseInt(addedRaw, 10) || 0;
let removed = parseInt(removedRaw, 10) || 0;
@ -51,7 +76,7 @@ export function getNumStatInfo(message: string) {
// ":000000 100644 000000000 fc44b0a37 A public/logo192.png" -> ['A', 'public/logo192.png']
export function getRawInfo(message: string) {
return {
action:message[35],
action: message[35],
path: message.substring(37),
};
}

View file

@ -4,7 +4,11 @@ import IHashMap from 'ts/interfaces/HashMap';
import { ONE_DAY, ONE_WEEK } from 'ts/helpers/formatter';
import getCommitInfo, { clearCache } from './getCommitInfo';
import { getInfoFromPath, getNumStatInfo, getRawInfo } from './getFileChanges';
import {
getInfoFromPath,
getNumStatInfo,
getRawInfo,
} from './getFileChanges';
function updateLineTotal(commit: any, line: any) {
commit.added += line.addedLines || 0;
@ -12,50 +16,60 @@ function updateLineTotal(commit: any, line: any) {
commit.changes += line.changedLines || 0;
}
function isNumStatLine(message: string) {
return message[1] === '\t'
|| message[2] === '\t'
|| message[3] === '\t'
|| message[4] === '\t'
|| message[5] === '\t'
|| message[6] === '\t'
|| message[7] === '\t';
}
export default function Parser(report: string[]) {
let commit = null;
const commits: Array<ICommit | ISystemCommit> = [];
let refEmailAuthor: IHashMap<string> = {};
let files: IHashMap<IFileChange> = {};
let files: Map<string, IFileChange> = new Map();
let fileChanges: IFileChange | null = null;
let firstMonday = 0;
clearCache();
for (let i = 0, l = report.length; i < l; i += 1) {
const message = report[i];
if (!message) continue;
const index = message.indexOf('\t');
if (index > 0 && index < 10) {
if (message[0] === ':') {
// парсинг файлов формата --raw
// ":000000 100644 0000000 496d1ef A .browserlistrc"
const line = getRawInfo(message);
fileChanges = files.get(line.path) as IFileChange;
if (!fileChanges) {
fileChanges = getInfoFromPath(line.path);
files.set(line.path, fileChanges);
}
fileChanges.action = line.action;
} else if (isNumStatLine(message)) {
// парсинг файлов формата --num-stat
// "1 0 .browserlistrc"
const line = getNumStatInfo(message);
if (!files[line.path]) {
files[line.path] = getInfoFromPath(line.path);
fileChanges = files.get(line.path) as IFileChange;
if (!fileChanges) {
fileChanges = getInfoFromPath(line.path);
files.set(line.path, fileChanges);
}
fileChanges = files[line.path];
fileChanges.addedLines = line.addedLines;
fileChanges.removedLines = line.removedLines;
fileChanges.changedLines = line.changedLines;
updateLineTotal(commit, line);
} else if (message[0] === ':') {
// парсинг файлов формата --raw
// ":000000 100644 0000000 496d1ef A .browserlistrc"
const line = getRawInfo(message);
if (!files[line.path]) {
files[line.path] = getInfoFromPath(line.path);
}
fileChanges = files[line.path];
fileChanges.action = line.action;
} else {
// парсинг коммита
// "2021-02-09T16:08:15+03:00>Albert>instein@mail.de>feat(init): added the speed of light"
if (commit) commit.fileChanges = Object.values(files);
files = {};
if (commit) commit.fileChanges = Array.from(files.values());
files.clear();
commit = getCommitInfo(message, refEmailAuthor);
const monday = commit.milliseconds - commit.day * ONE_DAY;
@ -69,5 +83,7 @@ export default function Parser(report: string[]) {
}
}
clearCache();
return commits;
}

View file

@ -26,8 +26,9 @@ export interface ILog {
week: number; // 42,
// user
author: string; // "Frolov Ivan",
email: string; // "frolov@mail.ru",
author: string; // "Dart Vader",
email: string; // "d.vader@emap.com",
company: string; // "emap",
// task
message: string; // "JIRA-0000 fix(profile): add new avatar",

View file

@ -1,3 +1,5 @@
export default interface IHashMap<T> {
[key: string | number]: T;
}
export type HashMap<T> = Map<string | number | undefined | null, T>;

View file

@ -28,14 +28,14 @@ import Recommendations from 'ts/components/Recommendations';
import { getMax, getMaxByLength } from 'ts/pages/Common/helpers/getMax';
import Description from 'ts/components/Description';
interface IAuthorViewProps {
interface AuthorViewProps {
response?: IPagination<any>;
updateSort?: Function;
rowsForExcel?: any[];
mode?: string;
}
function AuthorView({ response, updateSort, rowsForExcel, mode }: IAuthorViewProps) {
export function AuthorView({ response, updateSort, rowsForExcel, mode }: AuthorViewProps) {
const { t } = useTranslation();
if (!response) return null;
@ -58,6 +58,7 @@ function AuthorView({ response, updateSort, rowsForExcel, mode }: IAuthorViewPro
rows={response.content}
sort={response.sort}
updateSort={updateSort}
mode={mode}
type={mode === 'print' ? 'cards' : undefined}
columnCount={mode === 'print' ? 3 : undefined}
>
@ -84,6 +85,13 @@ function AuthorView({ response, updateSort, rowsForExcel, mode }: IAuthorViewPro
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"

View file

@ -0,0 +1,143 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ICommit from 'ts/interfaces/Commit';
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.author.type.work'),
t('page.team.author.type.dismissed'),
];
const taskChart = getOptions({ max: getMax(response, 'totalTasks'), 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"
title="page.team.pr.author"
width={200}
/>
<Column
title="page.team.author.status"
formatter={(row: any) => (row.isActive ? works : dismissed)}
template={(value: string) => <UiKitTags value={value} />}
width={100}
/>
{/*<Column*/}
{/* template={ColumnTypesEnum.SHORT_NUMBER}*/}
{/* title="page.team.company.people"*/}
{/* properties="totalEmployments"*/}
{/* width={90}*/}
{/*/>*/}
<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}
properties="totalDays"
width={90}
/>
<Column
isSortable="totalDays"
title="page.team.author.daysAll"
properties="totalDays"
width={150}
template={(value: number) => (
<LineChart
options={daysChart}
value={value}
/>
)}
/>
<Column
template={ColumnTypesEnum.SHORT_NUMBER}
properties="totalTasks"
width={90}
/>
<Column
isSortable="totalTasks"
title="page.team.author.tasks"
properties="totalTasks"
width={150}
template={(value: number) => (
<LineChart
options={taskChart}
value={value}
/>
)}
/>
<Column
properties="emptyCell"
minWidth={40}
/>
</DataView>
);
}
Companies.defaultProps = {
response: undefined,
};
export default Companies;

View file

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

View file

@ -0,0 +1,45 @@
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 Companies from './Companies';
const Company = observer(({
mode,
}: ICommonPageProps): React.ReactElement | null => {
const rows = dataGripStore.dataGrip.company.statistic;
if (!rows?.length) {
return mode !== 'print' ? (<NothingFound />) : null;
}
return (
<>
<Title title="page.team.author.title"/>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
content: rows, pagination, sort, mode,
})}
watch={`${mode}${dataGripStore.hash}`}
>
<Companies
mode={mode}
rowsForExcel={rows}
/>
<Pagination />
</DataLoader>
</>
);
});
export default Company;

View file

@ -50,7 +50,7 @@ function ReleaseView({ response, updateSort, rowsForExcel, mode }: IReleaseViewP
width={40}
formatter={(row: any) => {
const content = row.pr.map((commit: any) => (
dataGripStore?.dataGrip?.pr?.pr?.[commit.prId]
dataGripStore?.dataGrip?.pr?.pr?.get(commit.prId)
)).filter((item: any) => item?.firstCommit);
return (
<AllPR // @ts-ignore

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import ISort from 'ts/interfaces/Sort';
@ -17,10 +17,14 @@ import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import UiKitTags from 'ts/components/UiKit/components/Tags';
import { PRLink, TaskLink } from 'ts/components/ExternalLink';
import Title from 'ts/components/Title';
import PageWrapper from 'ts/components/Page/wrapper';
import { getMax } from 'ts/pages/Common/helpers/getMax';
import { getDate } from 'ts/helpers/formatter';
import TasksFilters from './TasksFilters';
interface ITasksViewProps {
response?: IPagination<any>;
updateSort?: Function;
@ -139,9 +143,19 @@ 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>
<TasksFilters
filters={filters}
onChange={setFilters}
/>
</PageWrapper>
<DataLoader
to="response"
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
@ -149,15 +163,13 @@ const Tasks = observer(({
})}
watch={`${mode}${dataGripStore.hash}`}
>
<br/>
<br/>
<br/>
<TasksView
mode={mode}
rowsForExcel={rows}
/>
<Pagination />
</DataLoader>
</>
);
});

View file

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

View file

@ -8,6 +8,7 @@ import fullScreen from 'ts/store/FullScreen';
import Author from './components/Author';
import Commits from './components/Commits';
import Company from './components/Company';
import Changes from './components/Changes';
import Hours from './components/Hours';
import PopularWords from './components/PopularWords';
@ -37,6 +38,7 @@ const View = observer(({ page }: ViewProps): React.ReactElement => {
if (page === 'total') return <Total/>;
if (page === 'scope') return <Scope mode={mode}/>;
if (page === 'author') return <Author mode={mode}/>;
if (page === 'company') return <Company mode={mode}/>;
if (page === 'type') return <Type mode={mode}/>;
if (page === 'pr') return <Pr mode={mode}/>;
if (page === 'day') return <Tempo/>;

View file

@ -94,7 +94,6 @@ const Main = observer(() => {
const view = viewNameStore.view;
useEffect(() => {
console.log('main');
// @ts-ignore
const list = window?.report || [];
if (list?.length && bugInReactWithDoubleInit !== list?.length) {

View file

@ -37,8 +37,8 @@ class DataGripStore {
hash: observable,
isDepersonalized: observable,
asyncSetCommits: action,
processingStep01: action,
processingStep03: action,
processingStringToCommit: action,
processingDataAnalysis: action,
depersonalized: action,
updateStatistic: action,
});
@ -47,10 +47,10 @@ class DataGripStore {
asyncSetCommits(dump?: string[]) {
if (!dump?.length) return;
splashScreenStore.show();
setTimeout(() => this.processingStep01(dump), PROCESSING_DELAY);
setTimeout(() => this.processingStringToCommit(dump), PROCESSING_DELAY);
}
processingStep01(dump?: string[]) {
processingStringToCommit(dump?: string[]) {
dataGrip.clear();
fileGrip.clear();
@ -60,20 +60,20 @@ class DataGripStore {
return;
}
setTimeout(() => this.processingStep02(commits), PROCESSING_DELAY);
setTimeout(() => this.processingCommitGrouping(commits), PROCESSING_DELAY);
}
processingStep02(commits: (ICommit | ISystemCommit)[]) {
processingCommitGrouping(commits: (ICommit | ISystemCommit)[]) {
commits.sort((a, b) => a.milliseconds - b.milliseconds);
commits.forEach((commit: ICommit | ISystemCommit) => {
dataGrip.addCommit(commit);
fileGrip.addCommit(commit);
});
setTimeout(() => this.processingStep03(commits), PROCESSING_DELAY);
setTimeout(() => this.processingDataAnalysis(commits), PROCESSING_DELAY);
}
processingStep03(commits: (ICommit | ISystemCommit)[]) {
processingDataAnalysis(commits: (ICommit | ISystemCommit)[]) {
fileGrip.updateTotalInfo();
this.commits = commits;

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: Parte de las estadísticas (la velocidad del trabajo, el dinero gastado, etc.) para los empleados con el tipo de "Asistente" no cuenta, ya que no es un rol permanente en el proyecto. Su trabajo es insignificante y puede ser ignorado.
§ page.team.author.description2: La clasificación predeterminada es la clasificación por número de tareas y grupos(empleados actuales, despedidos, ayudantes).
§ page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: Partie des statistiques (vitesse de travail, argent dépensé, etc.) pour les collaborateurs de type Assistant, ce nest pas une rôle permanente dans le projet. Leur travail est insignifiant et peut être ignoré.
§ page.team.author.description2: Le tri par défaut est le tri par nombre de tâches et de groupes (employés actuels, licenciés et aidants).
§ page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Part of the statistics* (work speed, costs, etc.) *for employees with the 'Assistant' type is not counted*, as it is an episodic role in the project. It is assumed that they do not affect the project, and their edits can be disregarded in the context of the overall volume of work.
§ page.team.author.description2: *Default sorting* is by the number of tasks and groups (current, fired, assisting employees).
§ page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: *Часть статистики* (скорость работы, затраченные деньги и т.п.) *по сотрудникам с типом «Помощник» не считается*, т.к. это эпизодическая роль в проекте. Предполагаем, что они не влияют на проект, а их правками можно пренебречь на фоне общего объема работы.
§ page.team.author.description2: *Сортировка по умолчанию* это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники).
§ page.team.author.status: Статус
§ page.team.author.company: Компания
§ page.team.author.firstCommit: Первый коммит
§ page.team.author.lastCommit: Последний
§ page.team.author.daysAll: Всего дней

View file

@ -24,6 +24,7 @@ export default `
§ page.team.author.description1: Часть статистики (скорость работы, затраченные деньги и т.п.) по сотрудникам с типом «Помощник» не считается, т.к. это не постоянная роль в проекте. Их работа незначительно и её можно не учитывать.
§ page.team.author.description2: Сортировка по умолчанию — это сортировка по количеству задач и группам (текущие, уволенные, помогающие сотрудники).
§ page.team.author.status: Status
§ page.team.author.company: Company
§ page.team.author.firstCommit: First commit
§ page.team.author.lastCommit: Last
§ page.team.author.daysAll: Total days