JIRA-0003 feat(some): add new charts

This commit is contained in:
bakhirev 2023-08-08 15:05:26 +03:00
parent fe82b15a2f
commit 42a6fbf363
33 changed files with 18430 additions and 10250 deletions

View file

@ -1,6 +1,6 @@
# [Assayo](https://assayo.jp/) # [Assayo](https://assayo.jp/)
Визуализация и анализ данных вашего git-репозитория ([демо](https://assayo.jp/demo/?dump=./test.git)). Визуализация и анализ данных вашего git-репозитория ([демо](https://assayo.jp/demo/?dump=./test.txt)).
##### Сотрудник может оценить новое место работы ##### Сотрудник может оценить новое место работы
- темп работы; - темп работы;
@ -44,26 +44,26 @@ Alex B <alex@mail.uk> <super_man@yahoo.com>
В корневой директории вашего проекта выполнить: В корневой директории вашего проекта выполнить:
``` ```
git --no-pager log --numstat --oneline --all --no-merges --reverse git --no-pager log --numstat --oneline --all --reverse
--date=iso-strict --pretty=format:"%ad>%cN>%cE>%s" --date=iso-strict --pretty=format:"%ad>%cN>%cE>%s"
| sed -e 's/\\/\\\\/g' | sed -e 's/`/"/g' | sed -e 's/\\/\\\\/g' | sed -e 's/`/"/g'
| sed -e 's/^/report.push(\`/g' | sed 's/$/\`\);/g' | sed -e 's/^/report.push(\`/g' | sed 's/$/\`\);/g'
| sed 's/\$/_/g' > dump.git | sed 's/\$/_/g' > log.txt
``` ```
Git создаст файл `dump.git`. Git создаст файл `log.txt`.
Он содержит данные для построения отчёта. Он содержит данные для построения отчёта.
### Как посмотреть отчёт онлайн? ### Как посмотреть отчёт онлайн?
- Перейти на [сайт](https://assayo.jp/) - Перейти на [сайт](https://assayo.jp/)
- Нажать кнопку «[Демо](https://assayo.jp/demo)» - Нажать кнопку «[Демо](https://assayo.jp/demo)»
- Перетащить файл `dump.git` в окно браузера - Перетащить файл `log.txt` в окно браузера
### Как посмотреть отчёт офлайн? ### Как посмотреть отчёт офлайн?
- Скачать этот репозиторий - Скачать этот репозиторий
- Перетащить файл `dump.git` в папку `/build` - Перетащить файл `log.txt` в папку `/build`
- Запустить `/build/index.html` - Запустить `/build/index.html`
- Или перетащить папку `/build` к себе в репозиторий (туда, где лежит `dump.git`). Можно сменить название. Например с `/build` на `/report` - Или перетащить папку `/build` к себе в репозиторий (туда, где лежит `log.txt`). Можно сменить название. Например с `/build` на `/report`
### Как пересобрать билд отчёта? ### Как пересобрать билд отчёта?
- Скачать этот репозиторий - Скачать этот репозиторий
@ -72,9 +72,9 @@ Git создаст файл `dump.git`.
- Свежая сборка будет в папке `/build` - Свежая сборка будет в папке `/build`
### Как посмотреть отчёт по группе микросервисов? ### Как посмотреть отчёт по группе микросервисов?
- Сгенерировать для каждого микросервиса `dump.git` (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.) - Сгенерировать для каждого микросервиса `log.txt` (`log-1.txt`, `log-2.txt`, `log-3.txt` и т.д.)
- См. «Как посмотреть отчёт онлайн?». На последнем шаге перетащить сразу все файлы в окно браузера. - См. «Как посмотреть отчёт онлайн?». На последнем шаге перетащить сразу все файлы в окно браузера.
- См. «Как посмотреть отчёт офлайн?». На втором шаге перетащить все файлы микросервисов (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.) в папку отчета (`/build`). - См. «Как посмотреть отчёт офлайн?». На втором шаге перетащить все файлы микросервисов (`log-1.txt`, `log-2.txt`, `log-3.txt` и т.д.) в папку отчета (`/build`).
### Как подписывать коммиты? ### Как подписывать коммиты?

View file

@ -1,17 +1,17 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.27b68202.css", "main.css": "./static/css/main.a871f7d5.css",
"main.js": "./static/js/main.fcc567df.js", "main.js": "./static/js/main.16986165.js",
"static/media/car.png": "./static/media/car.b8dd8738e37fe866285f.png", "static/media/car.png": "./static/media/car.b8dd8738e37fe866285f.png",
"index.html": "./index.html", "index.html": "./index.html",
"static/media/warning.svg": "./static/media/warning.e39a87773603f3ab157f.svg", "static/media/warning.svg": "./static/media/warning.e39a87773603f3ab157f.svg",
"static/media/info.svg": "./static/media/info.954631f6b19e3fe9c495.svg", "static/media/info.svg": "./static/media/info.954631f6b19e3fe9c495.svg",
"static/media/alert.svg": "./static/media/alert.41e2b99c481139c13074.svg", "static/media/alert.svg": "./static/media/alert.41e2b99c481139c13074.svg",
"main.27b68202.css.map": "./static/css/main.27b68202.css.map", "main.a871f7d5.css.map": "./static/css/main.a871f7d5.css.map",
"main.fcc567df.js.map": "./static/js/main.fcc567df.js.map" "main.16986165.js.map": "./static/js/main.16986165.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.27b68202.css", "static/css/main.a871f7d5.css",
"static/js/main.fcc567df.js" "static/js/main.16986165.js"
] ]
} }

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
<!doctype html><html lang="ru"><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="black-translucent"><script type="text/javascript">var report=[]</script><script src="/dump.git"></script><script src="./dump.git"></script><script src="../dump.git"></script><script src="./dump-0.git"></script><script src="./dump-1.git"></script><script src="./dump-2.git"></script><script src="./dump-3.git"></script><script src="./dump-4.git"></script><script src="./dump-5.git"></script><script src="./dump-6.git"></script><script src="./report/dump-0.git"></script><script src="./report/dump-1.git"></script><script src="./report/dump-2.git"></script><script src="./report/dump-3.git"></script><script src="./report/dump-4.git"></script><script src="./report/dump-5.git"></script><script src="./report/dump-6.git"></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>ASSAYO</title><meta name="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="keywords" content="git, статистика, аудит, история, log, мониторинг, контроль сотрудников"><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 Статистика"><meta name="msapplication-tooltip" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:title" content="GIT Статистика"><meta property="og:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="http://assayo.jp/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="GIT Статистика"><meta name="twitter:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.jp"><meta name="twitter:site" content="assayo.jp"><meta itemprop="name" content="GIT Статистика"><meta itemprop="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta itemprop="image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.fcc567df.js"></script><link href="./static/css/main.27b68202.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 lang="ru"><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="black-translucent"><script type="text/javascript">var report=[]</script><script src="/log.txt"></script><script src="./log.txt"></script><script src="../log.txt"></script><script src="./log-0.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="./report/log-0.txt"></script><script src="./report/log-1.txt"></script><script src="./report/log-2.txt"></script><script src="./report/log-3.txt"></script><script src="./report/log-4.txt"></script><script src="./report/log-5.txt"></script><script src="./report/log-6.txt"></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>ASSAYO</title><meta name="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="keywords" content="git, статистика, аудит, история, log, мониторинг, контроль сотрудников"><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 Статистика"><meta name="msapplication-tooltip" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:title" content="GIT Статистика"><meta property="og:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="http://assayo.jp/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="GIT Статистика"><meta name="twitter:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.jp"><meta name="twitter:site" content="assayo.jp"><meta itemprop="name" content="GIT Статистика"><meta itemprop="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta itemprop="image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.16986165.js"></script><link href="./static/css/main.a871f7d5.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -14,23 +14,23 @@
<script type="text/javascript"> <script type="text/javascript">
var report = []; var report = [];
</script> </script>
<script src='/dump.git'></script> <script src='/log.txt'></script>
<script src='./dump.git'></script> <script src='./log.txt'></script>
<script src='../dump.git'></script> <script src='../log.txt'></script>
<script src='./dump-0.git'></script> <script src='./log-0.txt'></script>
<script src='./dump-1.git'></script> <script src='./log-1.txt'></script>
<script src='./dump-2.git'></script> <script src='./log-2.txt'></script>
<script src='./dump-3.git'></script> <script src='./log-3.txt'></script>
<script src='./dump-4.git'></script> <script src='./log-4.txt'></script>
<script src='./dump-5.git'></script> <script src='./log-5.txt'></script>
<script src='./dump-6.git'></script> <script src='./log-6.txt'></script>
<script src='./report/dump-0.git'></script> <script src='./report/log-0.txt'></script>
<script src='./report/dump-1.git'></script> <script src='./report/log-1.txt'></script>
<script src='./report/dump-2.git'></script> <script src='./report/log-2.txt'></script>
<script src='./report/dump-3.git'></script> <script src='./report/log-3.txt'></script>
<script src='./report/dump-4.git'></script> <script src='./report/log-4.txt'></script>
<script src='./report/dump-5.git'></script> <script src='./report/log-5.txt'></script>
<script src='./report/dump-6.git'></script> <script src='./report/log-6.txt'></script>
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" /> <link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

View file

@ -60,24 +60,21 @@
bottom: 16px; bottom: 16px;
right: 16px; right: 16px;
font-size: 13px;
font-weight: 100;
display: block;
padding: 6px 12px;
line-height: 13px;
text-align: center;
cursor: pointer;
border-radius: 4px;
color: #8F8F8F;
border: 1px solid #F2F2F2;
background-color: #F2F2F2;
&:hover { &:hover {
bottom: 15px; bottom: 15px;
right: 15px; right: 15px;
background-color: #EDEDED;
} }
} }
} }
@media (max-width: 800px) {
.console {
width: 90%;
margin: 0 auto;
&_body {
height: 200px;
}
}
}

View file

@ -1,13 +1,29 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import Button from 'ts/components/UiKit/components/Button';
function copyInBuffer(value?: string) {
if (!value) return;
const copyTextarea = document.createElement('textarea');
copyTextarea.style.position = 'fixed';
copyTextarea.style.opacity = '0';
copyTextarea.textContent = value;
document.body.appendChild(copyTextarea);
copyTextarea.select();
document.execCommand('copy');
document.body.removeChild(copyTextarea);
}
import style from './index.module.scss'; import style from './index.module.scss';
interface IConsoleProps { interface IConsoleProps {
textForCopy?: string;
className?: string; className?: string;
children?: ReactNode; children?: ReactNode | string | number | null;
} }
function Console({ className, children }: IConsoleProps) { function Console({ className, textForCopy, children }: IConsoleProps) {
return ( return (
<div className={`${style.console} ${className || ''}`}> <div className={`${style.console} ${className || ''}`}>
<div className={`${style.console_header}`}> <div className={`${style.console_header}`}>
@ -16,16 +32,23 @@ function Console({ className, children }: IConsoleProps) {
<span className={`${style.console_header_icon}`}></span> <span className={`${style.console_header_icon}`}></span>
</div> </div>
<div className={`${style.console_body}`}> <div className={`${style.console_body}`}>
{children} {children || textForCopy}
</div> </div>
<button className={`${style.console_copy}`}> <Button
type="second"
className={`${style.console_copy}`}
onClick={() => {
copyInBuffer(textForCopy);
}}
>
Копировать Копировать
</button> </Button>
</div> </div>
); );
} }
Console.defaultProps = { Console.defaultProps = {
textForCopy: undefined,
children: undefined, children: undefined,
className: '', className: '',
}; };

View file

@ -54,6 +54,7 @@ export function getDayText(events: IHashMap<any>, timestamp: string): string {
function getRefAuthorByTime(list: any[], property: string) { function getRefAuthorByTime(list: any[], property: string) {
return list.reduce((refTimeAuthor: any, item: any) => { return list.reduce((refTimeAuthor: any, item: any) => {
if (item.isStaff) return refTimeAuthor; if (item.isStaff) return refTimeAuthor;
if (property === 'lastCommit' && !item.isDismissed) return refTimeAuthor;
const key = item?.[property]?.timestamp; const key = item?.[property]?.timestamp;
if (!refTimeAuthor[key]) refTimeAuthor[key] = []; if (!refTimeAuthor[key]) refTimeAuthor[key] = [];
refTimeAuthor[key].push(item.author); refTimeAuthor[key].push(item.author);

View file

@ -13,7 +13,7 @@ localization.parse('ru', `
§ sidebar.team.timestamp: Все коммиты § sidebar.team.timestamp: Все коммиты
§ sidebar.team.changes: Все изменения § sidebar.team.changes: Все изменения
§ sidebar.team.words: Популярные слова § sidebar.team.words: Популярные слова
§ sidebar.team.top: Рейтинг § sidebar.team.top: Викторина
§ sidebar.person.total: Общая информация § sidebar.person.total: Общая информация
§ sidebar.person.money: Стоимость работы § sidebar.person.money: Стоимость работы
§ sidebar.person.speed: Скорость § sidebar.person.speed: Скорость

View file

@ -0,0 +1,41 @@
import { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
import IHashMap from 'ts/interfaces/HashMap';
export default class DataGripByPr {
list: string[] = [];
pr: IHashMap<any> = {};
statistic: any = [];
clear() {
this.list = [];
this.pr = {};
this.statistic = [];
}
addCommit(commit: ISystemCommit) {
if (commit.commitType === COMMIT_TYPE.AUTO_MERGE) return;
if (this.pr[commit.prId]) {
this.#updateCommitByPR(commit);
} else {
this.#addCommitByPR(commit);
}
}
#updateCommitByPR(commit: ISystemCommit) {
const statistic = this.pr[commit.prId];
const property = commit.commitType === COMMIT_TYPE.MERGE ? 'close' : 'open';
statistic[property] = commit;
statistic.delay = statistic.open.milliseconds - statistic.close.milliseconds;
}
#addCommitByPR(commit: ISystemCommit) {
const property = commit.commitType === COMMIT_TYPE.MERGE ? 'close' : 'open';
this.pr[commit.prId] = { [property]: commit };
}
updateTotalInfo() {
this.statistic = [];
}
}

View file

@ -1,4 +1,4 @@
import ICommit from 'ts/interfaces/Commit'; import ICommit, { ISystemCommit } from 'ts/interfaces/Commit';
import settingsStore from 'ts/store/Settings'; import settingsStore from 'ts/store/Settings';
import Recommendations from 'ts/helpers/Recommendations'; import Recommendations from 'ts/helpers/Recommendations';
@ -10,6 +10,7 @@ import DataGripByTimestamp from './components/timestamp';
import DataGripByWeek from './components/week'; import DataGripByWeek from './components/week';
import MinMaxCounter from './components/counter'; import MinMaxCounter from './components/counter';
import DataGripByExtension from './components/extension'; import DataGripByExtension from './components/extension';
import DataGripByPR from './components/pr';
class DataGrip { class DataGrip {
firstLastCommit: any = new MinMaxCounter(); firstLastCommit: any = new MinMaxCounter();
@ -30,6 +31,8 @@ class DataGrip {
extension: any = new DataGripByExtension(); extension: any = new DataGripByExtension();
pr: any = new DataGripByPR();
initializationInfo: any = {}; initializationInfo: any = {};
clear() { clear() {
@ -42,10 +45,14 @@ class DataGrip {
this.week.clear(); this.week.clear();
this.recommendations.clear(); this.recommendations.clear();
this.extension.clear(); this.extension.clear();
this.pr.clear();
} }
addCommit(commit: ICommit) { addCommit(commit: ICommit | ISystemCommit) {
if (commit.author === 'GitHub') return; if (commit.author === 'GitHub') return; // @ts-ignore
if (commit.commitType) {
this.pr.addCommit(commit);
} else {
this.firstLastCommit.update(commit.milliseconds, commit); this.firstLastCommit.update(commit.milliseconds, commit);
this.author.addCommit(commit); this.author.addCommit(commit);
this.scope.addCommit(commit); this.scope.addCommit(commit);
@ -53,6 +60,7 @@ class DataGrip {
this.timestamp.addCommit(commit); this.timestamp.addCommit(commit);
this.week.addCommit(commit); this.week.addCommit(commit);
} }
}
#updateTotalInfo() { #updateTotalInfo() {
this.author.updateTotalInfo(); this.author.updateTotalInfo();
@ -62,6 +70,7 @@ class DataGrip {
this.timestamp.updateTotalInfo(this.author); this.timestamp.updateTotalInfo(this.author);
this.week.updateTotalInfo(this.author); this.week.updateTotalInfo(this.author);
this.recommendations.updateTotalInfo(this); this.recommendations.updateTotalInfo(this);
this.pr.updateTotalInfo(this);
} }
updateByInitialization() { updateByInitialization() {

View file

@ -1,6 +1,6 @@
import { IDirtyFile } from 'ts/interfaces/FileInfo'; import { IDirtyFile } from 'ts/interfaces/FileInfo';
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import ICommit from 'ts/interfaces/Commit'; import ICommit, { ISystemCommit } from 'ts/interfaces/Commit';
import settingsStore from 'ts/store/Settings'; import settingsStore from 'ts/store/Settings';
import getUserInfo from './user_info'; import getUserInfo from './user_info';
@ -13,7 +13,7 @@ export default function Parser(
parseCommit: Function, parseCommit: Function,
) { ) {
const allFiles: IHashMap<IDirtyFile> = {}; const allFiles: IHashMap<IDirtyFile> = {};
const commits: ICommit[] = []; const commits: Array<ICommit | ISystemCommit> = [];
let week: number = 0; let week: number = 0;
let weekEndTime: number = 0; let weekEndTime: number = 0;
@ -80,13 +80,12 @@ export default function Parser(
added = 0; added = 0;
removed = 0; removed = 0;
} }
if (prev) { if (prev) { // @ts-ignore
prev.changes += changes; prev.changes += changes; // @ts-ignore
prev.added += added; prev.added += added; // @ts-ignore
prev.removed += removed; prev.removed += removed;
} }
} else { } else {
if (prev) { if (prev) {
if (uniq[prev.date]) { if (uniq[prev.date]) {
// console.log(`double ${uniq[prev.date]} === ${i}`); // console.log(`double ${uniq[prev.date]} === ${i}`);
@ -104,8 +103,8 @@ export default function Parser(
next.week = week; next.week = week;
prev = next; prev = next;
commits.push(prev); commits.push(prev); // @ts-ignore
isFileInfo = true; isFileInfo = !prev.commitType;
} }
} }
if (prev) parseCommit(prev); if (prev) parseCommit(prev);

View file

@ -1,4 +1,4 @@
import ICommit from 'ts/interfaces/Commit'; import ICommit, { COMMIT_TYPE, ISystemCommit } from 'ts/interfaces/Commit';
function getTypeAndScope(messageParts: string[], task: string) { function getTypeAndScope(messageParts: string[], task: string) {
if (messageParts.length < 2) return ['', '']; if (messageParts.length < 2) return ['', ''];
@ -13,7 +13,12 @@ function getTypeAndScope(messageParts: string[], task: string) {
: [type, scope]; : [type, scope];
} }
export default function getUserInfo(logString: string): ICommit { // ABC-123, #123, gh-123
function getTask(message: string) {
return ((message || '').match(/(([A-Z]+-)|(#)|(gh-)|(GH-))([0-9]+)/gm) || [])[0] || '';
}
export default function getUserInfo(logString: string): ICommit | ISystemCommit {
// "2021-02-09T12:59:17+03:00>Frolov Ivan>frolov@mail.ru>profile" // "2021-02-09T12:59:17+03:00>Frolov Ivan>frolov@mail.ru>profile"
const parts = logString.split('>'); const parts = logString.split('>');
@ -26,11 +31,8 @@ export default function getUserInfo(logString: string): ICommit {
const email = parts.shift() || ''; const email = parts.shift() || '';
const message = parts.join('>'); const message = parts.join('>');
const task = (message.match(/(([A-Z]+-)|(#)|(gh-)|(GH-))([0-9]+)/gm) || [])[0] || ''; // ABC-123, #123, gh-123
const messageParts = message.split(':');
const [type, scope] = getTypeAndScope(messageParts, task);
return { const commonInfo: any = {
date: sourceDate, date: sourceDate,
day: day < 0 ? 6 : day, day: day < 0 ? 6 : day,
dayInMonth: date.getDate(), dayInMonth: date.getDate(),
@ -46,6 +48,50 @@ export default function getUserInfo(logString: string): ICommit {
email, email,
message, message,
type: 'не подписан',
scope: 'неопределенна',
};
const isSystemPR = message.indexOf('Pull request #') === 0;
const isSystemMerge = message.indexOf('Merge pull request #') === 0;
const fromGitHubToBitBucket = message.indexOf('Merge branch ') === 0;
const isSystemCommit = isSystemPR
|| isSystemMerge
|| fromGitHubToBitBucket
|| message.indexOf('Automatic merge from') === 0;
if (isSystemCommit) {
let commitType = COMMIT_TYPE.AUTO_MERGE;
let prId, repository, branch, toBranch, task;
if (isSystemMerge) {
commitType = COMMIT_TYPE.MERGE;
[, prId, repository, branch, toBranch ] = message
.replace(/(Merge\spull\srequest\s#)|(\sfrom\s)|(\sin\s)|(\sto\s)/gim, ',')
.split(',');
task = getTask(branch);
} else if (isSystemPR) {
commitType = COMMIT_TYPE.PR;
const messageParts = message.substring(14, Infinity).split(':');
prId = messageParts.shift();
task = getTask(messageParts.join(':'));
}
return {
...commonInfo,
prId: prId || '',
task: task || '',
repository: repository || '',
branch: branch || '',
toBranch: toBranch || '',
commitType,
};
}
const messageParts = message.split(':');
const task = getTask(message);
const [type, scope] = getTypeAndScope(messageParts, task);
return {
...commonInfo,
task, task,
type: type || 'не подписан', type: type || 'не подписан',
scope: scope || 'неопределенна', scope: scope || 'неопределенна',

View file

@ -1,4 +1,4 @@
export default interface ICommit { export interface ILog {
// date // date
date: string; // "2021-02-09T12:59:17+03:00", date: string; // "2021-02-09T12:59:17+03:00",
day: number; // 1, day: number; // 1,
@ -20,7 +20,23 @@ export default interface ICommit {
task: string; // "JIRA-0000, task: string; // "JIRA-0000,
type: string; // feat|fix|docs|style|refactor|test|chore type: string; // feat|fix|docs|style|refactor|test|chore
scope: string; // table, sale, profile and etc. scope: string; // table, sale, profile and etc.
}
export const COMMIT_TYPE = {
PR: 'PR',
MERGE: 'MERGE',
AUTO_MERGE: 'AUTO_MERGE',
};
export interface ISystemCommit extends ILog {
prId: string; // "59"
repository: string; // "ASSA/jira-frontend"
branch: string; // "feature/JIRA-151-create-MainPage-without-Banners-slider"
toBranch: string; // "master"
commitType: string; // 'PR' | 'MERGE' | 'AUTO_MERGE';
}
export default interface ICommit extends ILog {
// files // files
changes: number; // 0, changes: number; // 0,
added: number; // 0, added: number; // 0,

View file

@ -19,7 +19,7 @@ const TITLES = {
timestamp: 'Все коммиты', timestamp: 'Все коммиты',
week: 'Распределение коммитов по дням недели', week: 'Распределение коммитов по дням недели',
words: 'Популярные слова в комментарии к коммиту', words: 'Популярные слова в комментарии к коммиту',
top: 'Рейтинг', top: 'Викторина',
settings: 'Настройки', settings: 'Настройки',
}, },
person: { person: {

View file

@ -2,6 +2,7 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import localization from 'ts/helpers/Localization'; import localization from 'ts/helpers/Localization';
import settingsForm from 'ts/pages/Settings/store/Form';
import style from '../../styles/sidebar.module.scss'; import style from '../../styles/sidebar.module.scss';
@ -26,6 +27,12 @@ function SideBarMenuItem({
className={`${style.sidebar_item} ${isSelected ? style.selected : ''}`} className={`${style.sidebar_item} ${isSelected ? style.selected : ''}`}
to={link} to={link}
id={`sidebar-menu-${id}`} id={`sidebar-menu-${id}`}
onClick={() => {
if (settingsForm.isEdited) {
settingsForm.clear();
settingsForm.setInitState(settingsForm.initState);
}
}}
> >
<img <img
className={style.sidebar_item_icon} className={style.sidebar_item_icon}

View file

@ -6,24 +6,19 @@ import Console from 'ts/components/Console';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
function MailMap(): React.ReactElement | null { function MailMap(): React.ReactElement | null {
const statistic = dataGripStore.dataGrip.author.statistic.map((item: any) => ( const items = dataGripStore.dataGrip.author.statistic.map((item: any) => (
<p key={item.author}> `${item.author} <${item.firstCommit.email}> <${item.firstCommit.email}>`
{`${item.author} <${item.firstCommit.email}> <${item.firstCommit.email}>`}
</p>
)); ));
const commands = items.map((text: string) => (<p key={text}>{text}</p>));
const commandsForCopy = items.join('\r\n');
return ( return (
<div className={style.races_track}> <div className={style.races_track}>
<Console> <Console textForCopy={commandsForCopy}>
{statistic} {commands}
</Console> </Console>
</div> </div>
); );
} }
MailMap.defaultProps = {
type: '',
canStart: false,
};
export default MailMap; export default MailMap;

View file

@ -37,12 +37,12 @@ function ScopeView({ response }: IScopeViewProps) {
width={200} width={200}
/> />
<Column <Column
template={ColumnTypesEnum.SHORT_NUMBER} template={ColumnTypesEnum.NUMBER}
title="page.team.scope.days" title="page.team.scope.days"
properties="days" properties="days"
/> />
<Column <Column
template={ColumnTypesEnum.SHORT_NUMBER} template={ColumnTypesEnum.NUMBER}
title="page.team.scope.authorsDays" title="page.team.scope.authorsDays"
properties="authors" properties="authors"
formatter={(authors: any) => { formatter={(authors: any) => {
@ -58,7 +58,7 @@ function ScopeView({ response }: IScopeViewProps) {
formatter={(v: any[]) => (v?.length || 0)} formatter={(v: any[]) => (v?.length || 0)}
/> />
<Column <Column
template={ColumnTypesEnum.SHORT_NUMBER} template={ColumnTypesEnum.NUMBER}
title="page.team.scope.commits" title="page.team.scope.commits"
properties="commits" properties="commits"
/> />

View file

@ -3,18 +3,21 @@ import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip'; import dataGripStore from 'ts/store/DataGrip';
import Title from 'ts/components/Title'; import Achievement from 'ts/components/Achievement/components/Item';
import PageWrapper from 'ts/components/Page/wrapper'; import PageWrapper from 'ts/components/Page/wrapper';
import Achievements from 'ts/components/Achievement';
import Extension from 'ts/components/Extension'; import Extension from 'ts/components/Extension';
import Title from 'ts/components/Title';
import Races from 'ts/components/Races'; import Races from 'ts/components/Races';
import Tv100And1 from 'ts/components/Tv100And1'; import Tv100And1 from 'ts/components/Tv100And1';
import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type'; import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type';
import getAchievementByAuthor from 'ts/helpers/achievement/byAuthor'; import getAchievementByAuthor from 'ts/helpers/achievement/byAuthor';
import Description from 'ts/components/Description';
import { getDate } from 'ts/helpers/formatter'; import { getDate } from 'ts/helpers/formatter';
import style from '../styles/quiz.module.scss';
const Top = observer((): React.ReactElement => { const Top = observer((): React.ReactElement => {
const extensions = dataGripStore.dataGrip.extension.statistic const extensions = dataGripStore.dataGrip.extension.statistic
.slice(0, 4).map((statistic: any) => { .slice(0, 4).map((statistic: any) => {
@ -43,17 +46,26 @@ const Top = observer((): React.ReactElement => {
const achievements = getAchievementByAuthor(statistic.author); const achievements = getAchievementByAuthor(statistic.author);
const from = getDate(statistic.firstCommit.date); const from = getDate(statistic.firstCommit.date);
const to = getDate(statistic.lastCommit.date); const to = getDate(statistic.lastCommit.date);
return ( const achievementsList = [
<div key={statistic.author}>
<Title title={statistic.author}/>
{`Всего коммитов: ${statistic.commits} `}
{`Работал ${statistic.allDaysInProject} дней с ${from} по ${to} `}
<PageWrapper>
<Achievements list={[
...achievements[ACHIEVEMENT_TYPE.GOOD], ...achievements[ACHIEVEMENT_TYPE.GOOD],
...achievements[ACHIEVEMENT_TYPE.NORMAL], ...achievements[ACHIEVEMENT_TYPE.NORMAL],
...achievements[ACHIEVEMENT_TYPE.BAD], ...achievements[ACHIEVEMENT_TYPE.BAD],
]} /> ].map((type: string) => (
<Achievement
key={type}
type={type}
/>
));
return (
<div key={statistic.author}>
<Title title={statistic.author}/>
<Description text={`Всего коммитов: ${statistic.commits}`} />
<Description text={`Работал с ${from} по ${to} (${statistic.allDaysInProject} дней)`} />
<PageWrapper>
<div className={style.quiz_achievements}>
{achievementsList}
</div>
</PageWrapper> </PageWrapper>
</div> </div>
); );
@ -65,12 +77,12 @@ const Top = observer((): React.ReactElement => {
<Races tracks={tracks} /> <Races tracks={tracks} />
<Title title="Максимальная длинна подписи коммита"/> <Title title="Максимальная длинна подписи коммита"/>
<Tv100And1 rows={maxMessageLength} /> <Tv100And1 rows={maxMessageLength} />
{authors}
<PageWrapper> <PageWrapper>
<div style={{ whiteSpace: 'normal' }} > <div style={{ whiteSpace: 'normal' }} >
{extensions} {extensions}
</div> </div>
</PageWrapper> </PageWrapper>
{authors}
</> </>
); );
}); });

View file

@ -0,0 +1,9 @@
@import '../../../../styles/variables';
.quiz {
&_achievements {
margin: 12px 0 24px 0;
column-count: 3;
white-space: normal;
}
}

View file

@ -1,23 +0,0 @@
import React from 'react';
import style from '../styles/console.module.scss';
function Console() {
return (
<div className={`${style.welcome_console}`}>
<div className={`${style.welcome_console_header}`}>
<span className={`${style.welcome_console_header_icon}`}></span>
<span className={`${style.welcome_console_header_icon}`}></span>
<span className={`${style.welcome_console_header_icon}`}></span>
</div>
<div className={`${style.welcome_console_body}`}>
{'git --no-pager log --numstat --oneline --all --no-merges --reverse --date=iso-strict --pretty=format:"%ad>%cN>%cE>%s" | sed -e \'s/\\\\/\\\\\\\\/g\' | sed -e \'s/`/"/g\' | sed -e \'s/^/report.push(\\`/g\' | sed \'s/$/\\`\\);/g\' | sed \'s/\\$/_/g\' > dump.git\n'}
</div>
<button className={`${style.welcome_console_copy}`}>
Копировать
</button>
</div>
);
}
export default Console;

View file

@ -1,32 +1,36 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Console from './components/console'; import Console from 'ts/components/Console';
import style from './styles/index.module.scss'; import style from './styles/index.module.scss';
function Welcome() { function Welcome() {
const command = 'git --no-pager log --numstat --oneline --all --reverse --date=iso-strict --pretty=format:"%ad>%cN>%cE>%s" | sed -e \'s/\\\\/\\\\\\\\/g\' | sed -e \'s/`/"/g\' | sed -e \'s/^/report.push(\\`/g\' | sed \'s/$/\\`\\);/g\' | sed \'s/\\$/_/g\' > log.txt\n';
return ( return (
<section className={`${style.welcome}`}> <section className={style.welcome}>
<div className={`${style.welcome_row}`}> <div className={style.welcome_row}>
<h2 className={`${style.welcome__title_1}`}> <h2 className={style.welcome_first_title}>
Выполните команду в корне вашего проекта Выполните команду в корне вашего проекта
</h2> </h2>
<Console /> <Console
<p className={`${style.welcome__description}`}> className={style.welcome_console}
Git создаст файл dump.git. textForCopy={command}
/>
<p className={style.welcome_description}>
Git создаст файл log.txt.
Он содержит данные для построения отчёта. Он содержит данные для построения отчёта.
Или git shortlog -s -n -e если отчёт вам не нужен. Или git shortlog -s -n -e если отчёт вам не нужен.
Советую добавить в проект файл Добавьте файл
<Link <Link
className={`${style.welcome__description_link}`} className={`${style.welcome_link}`}
target="_blank" target="_blank"
to="https://git-scm.com/docs/gitmailmap"> to="https://git-scm.com/docs/gitmailmap">
.mailmap .mailmap
</Link> </Link>
{', чтобы обьединить статистику по пользователям.'} {' в проект, чтобы обьединить статистику по пользователям.'}
</p> </p>
<h2 className={`${style.welcome__title_2}`}> <h2 className={style.welcome_last_title}>
Перетащите файл dump.git на эту страницу Перетащите файл log.txt на эту страницу
</h2> </h2>
</div> </div>
</section> </section>

View file

@ -1,85 +0,0 @@
@import '../../../../styles/variables';
.welcome_console {
position: relative;
display: block;
width: 100%;
max-width: 700px;
margin: 0 auto;
box-sizing: border-box;
}
.welcome_console_header,
.welcome_console_body {
font-size: var(--font-s);
font-weight: 100;
display: block;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
line-height: 1.3;
text-align: left;
}
.welcome_console_header {
display: block;
height: 32px;
padding: 0 8px;
line-height: 20px;
color: #8F8F8F;
white-space: nowrap;
text-align: left;
border: 1px solid #D4D4D4;
border-bottom: none;
cursor: default;
background-color: #F2F2F2;
border-radius: 4px 4px 0 0;
}
.welcome_console_body {
height: 250px;
padding: 8px 16px 16px;
line-height: 1.3;
color: #00B200;
white-space: normal;
background-color: #0C0C0C;
border-radius: 0 0 4px 4px;
}
.welcome_console_header_icon {
display: inline-block;
width: 16px;
height: 16px;
margin: 6px 8px 0 0;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid #B5B5B5;
background: linear-gradient(90deg, #D7D8DB 0%, #B5B5B5 100%);
}
.welcome_console_copy {
position: absolute;
bottom: 16px;
right: 16px;
font-size: 13px;
font-weight: 100;
display: block;
padding: 6px 12px;
line-height: 13px;
text-align: center;
cursor: pointer;
border-radius: 4px;
color: #8F8F8F;
border: 1px solid #F2F2F2;
background-color: #F2F2F2;
}
.welcome_console_copy:hover {
bottom: 15px;
right: 15px;
background-color: #EDEDED;
}

View file

@ -13,53 +13,33 @@
box-sizing: border-box; box-sizing: border-box;
text-align: center; text-align: center;
&_console {
max-width: 700px;
} }
.welcome_row { &_row {
width: auto; width: auto;
} }
.welcome_step__icon, &_first_title,
.welcome_step__icon:after { &_last_title {
position: absolute;
top: 0;
left: 0;
font-size: 38px;
font-weight: 100;
display: block;
width: 70px;
height: 70px;
text-align: center;
line-height: 70px;
border: 1px solid black;
background-color: transparent;
}
.welcome_step__icon:after {
content: '';
top: 4px;
left: 4px;
background-color: transparent;
}
.welcome__title_1,
.welcome__title_2 {
font-size: 42px; font-size: 42px;
font-weight: 100; font-weight: 100;
margin: 46px auto; margin: 46px auto;
padding: 0; padding: 0;
} }
.welcome__title_1 { &_first_title {
margin-top: 0; margin-top: 0;
} }
.welcome__title_2 { &_last_title {
margin-bottom: 0; margin-bottom: 0;
} }
.welcome__description_link, &_link,
.welcome__description { &_description {
font-size: var(--font-xs); font-size: var(--font-xs);
display: inline-block; display: inline-block;
@ -74,34 +54,27 @@
color: #878FA1; color: #878FA1;
} }
.welcome__description_link { &_link {
display: inline; display: inline;
margin: 16px 0 0 4px;
text-decoration: underline; text-decoration: underline;
} }
}
@media (max-width: 800px) { @media (max-width: 800px) {
.welcome { .welcome {
display: block; display: block;
width: 100%; width: 100%;
padding: 32px 0 0 0; padding: 32px 0 0 0;
}
.welcome__title_1, &_first_title,
.welcome__title_2 { &_last_title {
width: 90%; width: 90%;
font-size: var(--font-l); font-size: var(--font-l);
} }
.welcome__description { &_description {
width: 90%; width: 90%;
} }
.welcome_icons__console {
width: 90%;
margin: 0 auto;
}
.welcome_icons__console_body {
height: 200px;
} }
} }