JIRA-1 feat(stat): add statistic

This commit is contained in:
bakhirev 2023-08-04 00:56:09 +03:00
parent 9bb99aec2e
commit fe82b15a2f
93 changed files with 24943 additions and 11394 deletions

View file

@ -1,6 +1,6 @@
# [Assayo](https://assayo.jp/) # [Assayo](https://assayo.jp/)
Визуализация и анализ данных вашего git-репозитория. Визуализация и анализ данных вашего git-репозитория ([демо](https://assayo.jp/demo/?dump=./test.git)).
##### Сотрудник может оценить новое место работы ##### Сотрудник может оценить новое место работы
- темп работы; - темп работы;
@ -38,7 +38,7 @@ Alex B <alex@mail.uk> <alex@gov.tk>
Alex B <alex@mail.uk> <bakhirev@ya.kz> Alex B <alex@mail.uk> <bakhirev@ya.kz>
Alex B <alex@mail.uk> <super_man@yahoo.com> Alex B <alex@mail.uk> <super_man@yahoo.com>
``` ```
Подробнее про формат этого файла можно прочитать тут [https://git-scm.com/docs/gitmailmap](gitmailmap). Подробнее про формат этого файла можно прочитать [тут](https://git-scm.com/docs/gitmailmap).
### Как выгрузить данные из git? ### Как выгрузить данные из git?
@ -56,7 +56,7 @@ Git создаст файл `dump.git`.
### Как посмотреть отчёт онлайн? ### Как посмотреть отчёт онлайн?
- Перейти на [сайт](https://assayo.jp/) - Перейти на [сайт](https://assayo.jp/)
- Нажать кнопку "[Демо](https://assayo.jp/demo)" - Нажать кнопку «[Демо](https://assayo.jp/demo)»
- Перетащить файл `dump.git` в окно браузера - Перетащить файл `dump.git` в окно браузера
### Как посмотреть отчёт офлайн? ### Как посмотреть отчёт офлайн?
@ -73,8 +73,8 @@ Git создаст файл `dump.git`.
### Как посмотреть отчёт по группе микросервисов? ### Как посмотреть отчёт по группе микросервисов?
- Сгенерировать для каждого микросервиса `dump.git` (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.) - Сгенерировать для каждого микросервиса `dump.git` (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.)
- См. "Как посмотреть отчёт онлайн?". На последнем шаге перетащить сразу все файлы в окно браузера. - См. «Как посмотреть отчёт онлайн?». На последнем шаге перетащить сразу все файлы в окно браузера.
- См. "Как посмотреть отчёт офлайн?". На втором шаге перетащить все файлы микросервисов (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.) в папку отчета (`/build`). - См. «Как посмотреть отчёт офлайн?». На втором шаге перетащить все файлы микросервисов (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.) в папку отчета (`/build`).
### Как подписывать коммиты? ### Как подписывать коммиты?
@ -87,15 +87,25 @@ JIRA-1234 feat(profile): Added avatar for user
- фича `(profile - раздел сайта, страница или новый функционал одним словом)` - фича `(profile - раздел сайта, страница или новый функционал одним словом)`
- какую проблему решали `(Added avatar for user)` - какую проблему решали `(Added avatar for user)`
### Как автоматизировать сбор данных (CI/CD)
#### Локально
- создайте клон нужного вам репозитория;
- скопируйте в корень папку `build`;
- откройте `build/index.html` в браузере и добавьте в закладки;
- добавьте ярлык на `build/assets/ci-cd.sh` в папку автозагрузки (Windows);
Каждый раз, при перезагрузке компьютера, скрипт будет обновлять статстику по всем данным, которые автоматически влились в основную ветку.
### RoadMap ### RoadMap
Релизы, примерно, раз в полгода. Что дальше: Релизы, примерно, раз в полгода. Что дальше:
- больше советов и достижений; - больше советов и достижений;
- итоги года / месяца, печать отчётов; - итоги года / месяца, печать отчётов;
- локализация и интернационализация;
- разные роли для статистики (скрытие финансов); - разные роли для статистики (скрытие финансов);
- разработка бекенда, интеграции с другими системами; - разработка бекенда, интеграции с другими системами;
- локализация и интернационализация;
### Пожелания, предложения, замечания ### Пожелания, предложения, замечания
- [alexey-bakhirev@yandex.ru](mailto:alexey-bakhirev@yandex.ru) - [alexey-bakhirev@yandex.ru](mailto:alexey-bakhirev@yandex.ru)

View file

@ -1,16 +1,17 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.33cc195e.css", "main.css": "./static/css/main.27b68202.css",
"main.js": "./static/js/main.405a9477.js", "main.js": "./static/js/main.fcc567df.js",
"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.33cc195e.css.map": "./static/css/main.33cc195e.css.map", "main.27b68202.css.map": "./static/css/main.27b68202.css.map",
"main.405a9477.js.map": "./static/js/main.405a9477.js.map" "main.fcc567df.js.map": "./static/js/main.fcc567df.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.33cc195e.css", "static/css/main.27b68202.css",
"static/js/main.405a9477.js" "static/js/main.fcc567df.js"
] ]
} }

View file

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><path class="cls-1" d="M60.51,27.13L22.64,88.62c-1.76,2.93,.39,6.83,3.9,6.83H102.28c3.51,0,5.66-3.9,3.9-6.83L68.32,27.13c-1.76-2.73-5.86-2.73-7.81,0Z"/><rect class="cls-1" x="61.42" y="41.65" width="5.86" height="32.28"/><rect class="cls-1" x="61.45" y="80.91" width="5.86" height="5.86"/></svg>

Before

Width:  |  Height:  |  Size: 535 B

View file

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><g><line class="cls-1" x1="82.18" y1="89.01" x2="94.08" y2="105.01"/><line class="cls-1" x1="84.13" y1="102.28" x2="87.06" y2="95.84"/><line class="cls-1" x1="90.76" y1="110.87" x2="84.13" y2="102.28"/><line class="cls-1" x1="94.08" y1="105.01" x2="90.76" y2="110.87"/><line class="cls-1" x1="94.28" y1="94.47" x2="87.06" y2="95.84"/><line class="cls-1" x1="100.72" y1="103.06" x2="94.28" y2="94.47"/><line class="cls-1" x1="94.28" y1="104.82" x2="100.72" y2="103.06"/></g><g><line class="cls-1" x1="77.49" y1="50.75" x2="99.94" y2="29.08"/><line class="cls-1" x1="98.38" y1="39.23" x2="91.55" y2="37.09"/><line class="cls-1" x1="105.99" y1="31.62" x2="98.38" y2="39.23"/><line class="cls-1" x1="99.94" y1="29.08" x2="105.99" y2="31.62"/><line class="cls-1" x1="89.4" y1="30.25" x2="91.55" y2="37.09"/><line class="cls-1" x1="97.21" y1="22.64" x2="89.4" y2="30.25"/><line class="cls-1" x1="99.74" y1="28.89" x2="97.21" y2="22.64"/></g><g><line class="cls-1" x1="57.19" y1="61.68" x2="17.18" y2="45.09"/><line class="cls-1" x1="27.33" y1="42.36" x2="27.91" y2="49.58"/><line class="cls-1" x1="17.37" y1="38.45" x2="27.33" y2="42.36"/><line class="cls-1" x1="17.18" y1="45.09" x2="17.37" y2="38.45"/><line class="cls-1" x1="22.45" y1="54.26" x2="27.91" y2="49.58"/><line class="cls-1" x1="12.49" y1="49.97" x2="22.45" y2="54.26"/><line class="cls-1" x1="17.18" y1="45.28" x2="12.49" y2="49.97"/></g><circle class="cls-1" cx="64.02" cy="64.61" r="7.42"/><path class="cls-1" d="M44.89,60.9c-.2,1.17-.39,2.54-.39,3.71,0,10.74,8.78,19.32,19.32,19.32s19.32-8.78,19.32-19.32-8.78-19.32-19.32-19.32c-6.44,0-12.49,3.12-16.01,8.39"/><path class="cls-1" d="M33.96,58.75c-.39,1.95-.59,3.9-.59,5.86,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-6.05-1.76-12.1-5.27-17.18"/><path class="cls-1" d="M80.22,38.84c-4.88-3.12-10.54-4.68-16.2-4.68-10.15,0-20.1,5.27-25.57,13.86"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 725 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="84.97" y1="58.68" x2="84.97" y2="24.47"/><line class="cls-1" x1="43.92" y1="58.68" x2="43.92" y2="34.88"/><line class="cls-1" x1="54.97" y1="24.47" x2="84.97" y2="24.47"/><line class="cls-1" x1="54.6" y1="38.3" x2="74.28" y2="38.3"/><line class="cls-1" x1="54.6" y1="46.78" x2="74.28" y2="46.78"/><line class="cls-1" x1="54.6" y1="55.26" x2="74.28" y2="55.26"/><line class="cls-1" x1="54.97" y1="24.47" x2="43.92" y2="34.88"/><line class="cls-1" x1="35.29" y1="64.88" x2="48.76" y2="64.88"/><line class="cls-1" x1="80.12" y1="64.88" x2="93.6" y2="64.88"/><line class="cls-1" x1="63.23" y1="80.85" x2="65.66" y2="80.85"/><line class="cls-1" x1="73.07" y1="80.85" x2="75.5" y2="80.85"/><line class="cls-1" x1="82.9" y1="80.85" x2="85.33" y2="80.85"/><line class="cls-1" x1="43.56" y1="80.85" x2="45.99" y2="80.85"/><line class="cls-1" x1="53.39" y1="80.85" x2="55.83" y2="80.85"/><line class="cls-1" x1="58.31" y1="86.2" x2="60.74" y2="86.2"/><line class="cls-1" x1="68.14" y1="86.2" x2="70.58" y2="86.2"/><line class="cls-1" x1="77.98" y1="86.2" x2="80.41" y2="86.2"/><line class="cls-1" x1="87.82" y1="86.2" x2="90.25" y2="86.2"/><line class="cls-1" x1="38.64" y1="86.2" x2="41.07" y2="86.2"/><line class="cls-1" x1="48.47" y1="86.2" x2="50.9" y2="86.2"/><path class="cls-1" d="M80.12,64.88c0,3.37-7.08,6.15-15.68,6.15s-15.4-2.65-15.67-5.95"/><line class="cls-1" x1="43.92" y1="55.26" x2="35.29" y2="55.26"/><line class="cls-1" x1="93.6" y1="55.26" x2="84.97" y2="55.26"/><line class="cls-1" x1="35.29" y1="75.94" x2="35.29" y2="55.26"/><line class="cls-1" x1="28.37" y1="71.03" x2="28.37" y2="60.69"/><line class="cls-1" x1="93.6" y1="75.94" x2="93.6" y2="55.26"/><rect class="cls-1" x="93.6" y="60.1" width="6.62" height="10.93"/><line class="cls-1" x1="35.29" y1="75.94" x2="93.6" y2="75.94"/><rect class="cls-1" x="24.62" y="91.55" width="79.65" height="11.16"/><line class="cls-1" x1="35.29" y1="75.94" x2="24.62" y2="91.55"/><line class="cls-1" x1="28.37" y1="71.03" x2="23.04" y2="78.84"/><line class="cls-1" x1="93.6" y1="75.94" x2="104.27" y2="91.55"/><line class="cls-1" x1="35.29" y1="60.1" x2="28.37" y2="60.1"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="50.16" y1="77.43" x2="34.16" y2="77.43"/><polyline class="cls-1" points="82.96 62.98 98.96 62.98 98.96 53.81 34.16 53.81 34.16 77.43"/><polygon class="cls-1" points="98.96 102.61 52.7 102.61 44.7 102.61 34.16 102.61 34.16 92.85 98.96 92.85 98.96 102.61"/><path class="cls-1" d="M40.4,77.43c7.03,9.37,0,15.42,0,15.42"/><path class="cls-1" d="M90.76,62.98c-16.2,17.96,0,29.86,0,29.86"/><path class="cls-1" d="M34.16,73.52l-18.74-10.54c-2.73-1.56-1.37-5.27,1.95-5.27h16.79v15.81Z"/><polygon class="cls-1" points="73 15.55 60.31 19.65 60.31 40.92 73 40.92 73 15.55"/><line class="cls-1" x1="56.22" y1="28.24" x2="60.31" y2="28.24"/><line class="cls-1" x1="73" y1="28.24" x2="102.87" y2="28.24"/><line class="cls-1" x1="38.84" y1="49.9" x2="30.84" y2="41.12"/><line class="cls-1" x1="40.8" y1="44.05" x2="30.84" y2="41.12"/><line class="cls-1" x1="38.65" y1="35.65" x2="40.8" y2="44.05"/><line class="cls-1" x1="45.09" y1="41.31" x2="38.65" y2="35.65"/><line class="cls-1" x1="78.86" y1="46.19" x2="85.3" y2="38.58"/><line class="cls-1" x1="86.86" y1="45.02" x2="85.3" y2="38.58"/><line class="cls-1" x1="95.45" y1="39.36" x2="86.86" y2="45.02"/><line class="cls-1" x1="92.52" y1="49.71" x2="95.45" y2="39.36"/><line class="cls-1" x1="46.46" y1="32.92" x2="45.09" y2="41.31"/><line class="cls-1" x1="53.29" y1="45.61" x2="46.46" y2="32.92"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="66.07" y1="23.55" x2="66.07" y2="58.88"/><line class="cls-1" x1="66.07" y1="64.74" x2="66.07" y2="95.58"/><line class="cls-1" x1="54.56" y1="76.45" x2="54.56" y2="95.58"/><line class="cls-1" x1="42.84" y1="76.45" x2="42.84" y2="95.58"/><line class="cls-1" x1="99.84" y1="31.16" x2="66.07" y2="31.16"/><line class="cls-1" x1="99.84" y1="51.07" x2="66.07" y2="51.07"/><line class="cls-1" x1="99.84" y1="64.74" x2="23.72" y2="64.74"/><circle class="cls-1" cx="42.65" cy="39.75" r="13.86"/><path class="cls-1" d="M79.74,64.74c-1.95,10.74-2.15,27.13,12.49,36.31"/></svg>

After

Width:  |  Height:  |  Size: 829 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><circle class="cls-1" cx="63.43" cy="66.41" r="19.33"/><g><line class="cls-1" x1="81.58" y1="90.8" x2="93.49" y2="106.81"/><line class="cls-1" x1="83.54" y1="104.08" x2="86.46" y2="97.64"/><line class="cls-1" x1="90.17" y1="112.67" x2="83.54" y2="104.08"/><line class="cls-1" x1="93.49" y1="106.81" x2="90.17" y2="112.67"/><line class="cls-1" x1="93.69" y1="96.27" x2="86.46" y2="97.64"/><line class="cls-1" x1="100.13" y1="104.86" x2="93.69" y2="96.27"/><line class="cls-1" x1="93.69" y1="106.62" x2="100.13" y2="104.86"/></g><g><line class="cls-1" x1="76.9" y1="52.55" x2="99.35" y2="30.88"/><line class="cls-1" x1="97.79" y1="41.03" x2="90.95" y2="38.88"/><line class="cls-1" x1="105.4" y1="33.42" x2="97.79" y2="41.03"/><line class="cls-1" x1="99.35" y1="30.88" x2="105.4" y2="33.42"/><line class="cls-1" x1="88.81" y1="32.05" x2="90.95" y2="38.88"/><line class="cls-1" x1="96.61" y1="24.44" x2="88.81" y2="32.05"/><line class="cls-1" x1="99.15" y1="30.69" x2="96.61" y2="24.44"/></g><g><line class="cls-1" x1="45.6" y1="58.92" x2="16.59" y2="46.89"/><line class="cls-1" x1="26.74" y1="44.15" x2="27.32" y2="51.38"/><line class="cls-1" x1="16.78" y1="40.25" x2="26.74" y2="44.15"/><line class="cls-1" x1="16.59" y1="46.89" x2="16.78" y2="40.25"/><line class="cls-1" x1="21.86" y1="56.06" x2="27.32" y2="51.38"/><line class="cls-1" x1="11.9" y1="51.77" x2="21.86" y2="56.06"/><line class="cls-1" x1="16.59" y1="47.08" x2="11.9" y2="51.77"/></g><circle class="cls-1" cx="63.43" cy="66.41" r="7.42"/><path class="cls-1" d="M33.29,60.98c-.34,1.81-.5,3.62-.5,5.43,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-5.82-1.63-11.64-4.88-16.6"/><path class="cls-1" d="M79.63,40.64c-4.88-3.12-10.54-4.68-16.2-4.68-9.94,0-19.7,5.06-25.23,13.34"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

2
build/assets/ci-cd.sh Normal file
View file

@ -0,0 +1,2 @@
git pull
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

File diff suppressed because it is too large Load diff

BIN
build/assets/games/car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

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.405a9477.js"></script><link href="./static/css/main.33cc195e.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="/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>

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

View file

@ -0,0 +1,74 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @remix-run/router v1.3.1
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router DOM v6.8.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.8.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><path class="cls-1" d="M60.51,27.13L22.64,88.62c-1.76,2.93,.39,6.83,3.9,6.83H102.28c3.51,0,5.66-3.9,3.9-6.83L68.32,27.13c-1.76-2.73-5.86-2.73-7.81,0Z"/><rect class="cls-1" x="61.42" y="41.65" width="5.86" height="32.28"/><rect class="cls-1" x="61.45" y="80.91" width="5.86" height="5.86"/></svg>

Before

Width:  |  Height:  |  Size: 535 B

View file

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><g><line class="cls-1" x1="82.18" y1="89.01" x2="94.08" y2="105.01"/><line class="cls-1" x1="84.13" y1="102.28" x2="87.06" y2="95.84"/><line class="cls-1" x1="90.76" y1="110.87" x2="84.13" y2="102.28"/><line class="cls-1" x1="94.08" y1="105.01" x2="90.76" y2="110.87"/><line class="cls-1" x1="94.28" y1="94.47" x2="87.06" y2="95.84"/><line class="cls-1" x1="100.72" y1="103.06" x2="94.28" y2="94.47"/><line class="cls-1" x1="94.28" y1="104.82" x2="100.72" y2="103.06"/></g><g><line class="cls-1" x1="77.49" y1="50.75" x2="99.94" y2="29.08"/><line class="cls-1" x1="98.38" y1="39.23" x2="91.55" y2="37.09"/><line class="cls-1" x1="105.99" y1="31.62" x2="98.38" y2="39.23"/><line class="cls-1" x1="99.94" y1="29.08" x2="105.99" y2="31.62"/><line class="cls-1" x1="89.4" y1="30.25" x2="91.55" y2="37.09"/><line class="cls-1" x1="97.21" y1="22.64" x2="89.4" y2="30.25"/><line class="cls-1" x1="99.74" y1="28.89" x2="97.21" y2="22.64"/></g><g><line class="cls-1" x1="57.19" y1="61.68" x2="17.18" y2="45.09"/><line class="cls-1" x1="27.33" y1="42.36" x2="27.91" y2="49.58"/><line class="cls-1" x1="17.37" y1="38.45" x2="27.33" y2="42.36"/><line class="cls-1" x1="17.18" y1="45.09" x2="17.37" y2="38.45"/><line class="cls-1" x1="22.45" y1="54.26" x2="27.91" y2="49.58"/><line class="cls-1" x1="12.49" y1="49.97" x2="22.45" y2="54.26"/><line class="cls-1" x1="17.18" y1="45.28" x2="12.49" y2="49.97"/></g><circle class="cls-1" cx="64.02" cy="64.61" r="7.42"/><path class="cls-1" d="M44.89,60.9c-.2,1.17-.39,2.54-.39,3.71,0,10.74,8.78,19.32,19.32,19.32s19.32-8.78,19.32-19.32-8.78-19.32-19.32-19.32c-6.44,0-12.49,3.12-16.01,8.39"/><path class="cls-1" d="M33.96,58.75c-.39,1.95-.59,3.9-.59,5.86,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-6.05-1.76-12.1-5.27-17.18"/><path class="cls-1" d="M80.22,38.84c-4.88-3.12-10.54-4.68-16.2-4.68-10.15,0-20.1,5.27-25.57,13.86"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 725 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="84.97" y1="58.68" x2="84.97" y2="24.47"/><line class="cls-1" x1="43.92" y1="58.68" x2="43.92" y2="34.88"/><line class="cls-1" x1="54.97" y1="24.47" x2="84.97" y2="24.47"/><line class="cls-1" x1="54.6" y1="38.3" x2="74.28" y2="38.3"/><line class="cls-1" x1="54.6" y1="46.78" x2="74.28" y2="46.78"/><line class="cls-1" x1="54.6" y1="55.26" x2="74.28" y2="55.26"/><line class="cls-1" x1="54.97" y1="24.47" x2="43.92" y2="34.88"/><line class="cls-1" x1="35.29" y1="64.88" x2="48.76" y2="64.88"/><line class="cls-1" x1="80.12" y1="64.88" x2="93.6" y2="64.88"/><line class="cls-1" x1="63.23" y1="80.85" x2="65.66" y2="80.85"/><line class="cls-1" x1="73.07" y1="80.85" x2="75.5" y2="80.85"/><line class="cls-1" x1="82.9" y1="80.85" x2="85.33" y2="80.85"/><line class="cls-1" x1="43.56" y1="80.85" x2="45.99" y2="80.85"/><line class="cls-1" x1="53.39" y1="80.85" x2="55.83" y2="80.85"/><line class="cls-1" x1="58.31" y1="86.2" x2="60.74" y2="86.2"/><line class="cls-1" x1="68.14" y1="86.2" x2="70.58" y2="86.2"/><line class="cls-1" x1="77.98" y1="86.2" x2="80.41" y2="86.2"/><line class="cls-1" x1="87.82" y1="86.2" x2="90.25" y2="86.2"/><line class="cls-1" x1="38.64" y1="86.2" x2="41.07" y2="86.2"/><line class="cls-1" x1="48.47" y1="86.2" x2="50.9" y2="86.2"/><path class="cls-1" d="M80.12,64.88c0,3.37-7.08,6.15-15.68,6.15s-15.4-2.65-15.67-5.95"/><line class="cls-1" x1="43.92" y1="55.26" x2="35.29" y2="55.26"/><line class="cls-1" x1="93.6" y1="55.26" x2="84.97" y2="55.26"/><line class="cls-1" x1="35.29" y1="75.94" x2="35.29" y2="55.26"/><line class="cls-1" x1="28.37" y1="71.03" x2="28.37" y2="60.69"/><line class="cls-1" x1="93.6" y1="75.94" x2="93.6" y2="55.26"/><rect class="cls-1" x="93.6" y="60.1" width="6.62" height="10.93"/><line class="cls-1" x1="35.29" y1="75.94" x2="93.6" y2="75.94"/><rect class="cls-1" x="24.62" y="91.55" width="79.65" height="11.16"/><line class="cls-1" x1="35.29" y1="75.94" x2="24.62" y2="91.55"/><line class="cls-1" x1="28.37" y1="71.03" x2="23.04" y2="78.84"/><line class="cls-1" x1="93.6" y1="75.94" x2="104.27" y2="91.55"/><line class="cls-1" x1="35.29" y1="60.1" x2="28.37" y2="60.1"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="50.16" y1="77.43" x2="34.16" y2="77.43"/><polyline class="cls-1" points="82.96 62.98 98.96 62.98 98.96 53.81 34.16 53.81 34.16 77.43"/><polygon class="cls-1" points="98.96 102.61 52.7 102.61 44.7 102.61 34.16 102.61 34.16 92.85 98.96 92.85 98.96 102.61"/><path class="cls-1" d="M40.4,77.43c7.03,9.37,0,15.42,0,15.42"/><path class="cls-1" d="M90.76,62.98c-16.2,17.96,0,29.86,0,29.86"/><path class="cls-1" d="M34.16,73.52l-18.74-10.54c-2.73-1.56-1.37-5.27,1.95-5.27h16.79v15.81Z"/><polygon class="cls-1" points="73 15.55 60.31 19.65 60.31 40.92 73 40.92 73 15.55"/><line class="cls-1" x1="56.22" y1="28.24" x2="60.31" y2="28.24"/><line class="cls-1" x1="73" y1="28.24" x2="102.87" y2="28.24"/><line class="cls-1" x1="38.84" y1="49.9" x2="30.84" y2="41.12"/><line class="cls-1" x1="40.8" y1="44.05" x2="30.84" y2="41.12"/><line class="cls-1" x1="38.65" y1="35.65" x2="40.8" y2="44.05"/><line class="cls-1" x1="45.09" y1="41.31" x2="38.65" y2="35.65"/><line class="cls-1" x1="78.86" y1="46.19" x2="85.3" y2="38.58"/><line class="cls-1" x1="86.86" y1="45.02" x2="85.3" y2="38.58"/><line class="cls-1" x1="95.45" y1="39.36" x2="86.86" y2="45.02"/><line class="cls-1" x1="92.52" y1="49.71" x2="95.45" y2="39.36"/><line class="cls-1" x1="46.46" y1="32.92" x2="45.09" y2="41.31"/><line class="cls-1" x1="53.29" y1="45.61" x2="46.46" y2="32.92"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="66.07" y1="23.55" x2="66.07" y2="58.88"/><line class="cls-1" x1="66.07" y1="64.74" x2="66.07" y2="95.58"/><line class="cls-1" x1="54.56" y1="76.45" x2="54.56" y2="95.58"/><line class="cls-1" x1="42.84" y1="76.45" x2="42.84" y2="95.58"/><line class="cls-1" x1="99.84" y1="31.16" x2="66.07" y2="31.16"/><line class="cls-1" x1="99.84" y1="51.07" x2="66.07" y2="51.07"/><line class="cls-1" x1="99.84" y1="64.74" x2="23.72" y2="64.74"/><circle class="cls-1" cx="42.65" cy="39.75" r="13.86"/><path class="cls-1" d="M79.74,64.74c-1.95,10.74-2.15,27.13,12.49,36.31"/></svg>

After

Width:  |  Height:  |  Size: 829 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><circle class="cls-1" cx="63.43" cy="66.41" r="19.33"/><g><line class="cls-1" x1="81.58" y1="90.8" x2="93.49" y2="106.81"/><line class="cls-1" x1="83.54" y1="104.08" x2="86.46" y2="97.64"/><line class="cls-1" x1="90.17" y1="112.67" x2="83.54" y2="104.08"/><line class="cls-1" x1="93.49" y1="106.81" x2="90.17" y2="112.67"/><line class="cls-1" x1="93.69" y1="96.27" x2="86.46" y2="97.64"/><line class="cls-1" x1="100.13" y1="104.86" x2="93.69" y2="96.27"/><line class="cls-1" x1="93.69" y1="106.62" x2="100.13" y2="104.86"/></g><g><line class="cls-1" x1="76.9" y1="52.55" x2="99.35" y2="30.88"/><line class="cls-1" x1="97.79" y1="41.03" x2="90.95" y2="38.88"/><line class="cls-1" x1="105.4" y1="33.42" x2="97.79" y2="41.03"/><line class="cls-1" x1="99.35" y1="30.88" x2="105.4" y2="33.42"/><line class="cls-1" x1="88.81" y1="32.05" x2="90.95" y2="38.88"/><line class="cls-1" x1="96.61" y1="24.44" x2="88.81" y2="32.05"/><line class="cls-1" x1="99.15" y1="30.69" x2="96.61" y2="24.44"/></g><g><line class="cls-1" x1="45.6" y1="58.92" x2="16.59" y2="46.89"/><line class="cls-1" x1="26.74" y1="44.15" x2="27.32" y2="51.38"/><line class="cls-1" x1="16.78" y1="40.25" x2="26.74" y2="44.15"/><line class="cls-1" x1="16.59" y1="46.89" x2="16.78" y2="40.25"/><line class="cls-1" x1="21.86" y1="56.06" x2="27.32" y2="51.38"/><line class="cls-1" x1="11.9" y1="51.77" x2="21.86" y2="56.06"/><line class="cls-1" x1="16.59" y1="47.08" x2="11.9" y2="51.77"/></g><circle class="cls-1" cx="63.43" cy="66.41" r="7.42"/><path class="cls-1" d="M33.29,60.98c-.34,1.81-.5,3.62-.5,5.43,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-5.82-1.63-11.64-4.88-16.6"/><path class="cls-1" d="M79.63,40.64c-4.88-3.12-10.54-4.68-16.2-4.68-9.94,0-19.7,5.06-25.23,13.34"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

2
public/assets/ci-cd.sh Normal file
View file

@ -0,0 +1,2 @@
git pull
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

File diff suppressed because it is too large Load diff

BIN
public/assets/games/car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
src/assets/games/car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

View file

@ -25,6 +25,8 @@ function getParametersFromString(text: string) {
} }
function renderReactApplication() { function renderReactApplication() {
// @ts-ignore
console.log(window?.report?.length);
render( render(
<React.StrictMode> <React.StrictMode>
<HashRouter> <HashRouter>

View file

@ -9,7 +9,7 @@
display: inline-block; display: inline-block;
width: 100%; width: 100%;
min-height: 80px; min-height: 80px;
padding: 16px 0 0 90px; padding: 23px 0 0 90px;
margin: 0 10px 10px 0; margin: 0 10px 10px 0;
box-sizing: border-box; box-sizing: border-box;
vertical-align: top; vertical-align: top;

View file

@ -0,0 +1,83 @@
@import '../../../styles/variables';
.console {
position: relative;
display: block;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
&_header,
&_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;
}
&_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;
&_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%);
}
}
&_body {
min-height: 250px;
padding: 8px 16px 16px;
line-height: 1.3;
color: #00B200;
white-space: normal;
background-color: #0C0C0C;
border-radius: 0 0 4px 4px;
}
&_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;
&:hover {
bottom: 15px;
right: 15px;
background-color: #EDEDED;
}
}
}

View file

@ -0,0 +1,33 @@
import React, { ReactNode } from 'react';
import style from './index.module.scss';
interface IConsoleProps {
className?: string;
children?: ReactNode;
}
function Console({ className, children }: IConsoleProps) {
return (
<div className={`${style.console} ${className || ''}`}>
<div className={`${style.console_header}`}>
<span className={`${style.console_header_icon}`}></span>
<span className={`${style.console_header_icon}`}></span>
<span className={`${style.console_header_icon}`}></span>
</div>
<div className={`${style.console_body}`}>
{children}
</div>
<button className={`${style.console_copy}`}>
Копировать
</button>
</div>
);
}
Console.defaultProps = {
children: undefined,
className: '',
};
export default Console;

View file

@ -45,7 +45,8 @@
font-size: var(--font-xs); font-size: var(--font-xs);
display: inline-block; display: inline-block;
margin: 0 0 var(--space-xs) 0; margin: 0 0 var(--space-xs) 0;
line-height: var(--font-s); line-height: var(--font-m);
vertical-align: top;
color: var(--color-42); color: var(--color-42);
} }
@ -53,6 +54,10 @@
margin-right: var(--space-l); margin-right: var(--space-l);
} }
&_message {
width: calc(100% - 44px);
}
&_row { &_row {
display: block; display: block;
padding: 0 0 0 32px; padding: 0 0 0 32px;

View file

@ -47,22 +47,35 @@ function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
} }
interface IDayInfoProps { interface IDayInfoProps {
day: IDayInfo, day: IDayInfo;
order: string[] order: string[];
events?: any;
timestamp?: string;
} }
function DayInfo({ day, order }: IDayInfoProps): React.ReactElement { function DayInfo({ day, order, events, timestamp }: IDayInfoProps): React.ReactElement {
const firstCommit = events?.firstCommit?.[timestamp || ''] || [];
const lastCommit = events?.lastCommit?.[timestamp || ''] || [];
let taskNumber = 0; let taskNumber = 0;
console.dir(firstCommit);
const items = Object.entries(day?.tasksByAuthor) const items = Object.entries(day?.tasksByAuthor)
.sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0]))) .sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0])))
.map(([author, tasks]: [string, any]) => { .map(([author, tasks]: [string, any]) => {
taskNumber += Object.keys(tasks).length; taskNumber += Object.keys(tasks).length;
let suffix = '';
if (firstCommit.includes(author)) suffix = '(первый рабочий день)';
if (lastCommit.includes(author)) suffix = '(последний рабочий день)';
return ( return (
<div <div
key={author} key={author}
className={style.day_info} className={style.day_info}
> >
<h3 className={style.day_info_author}>{author}</h3> <h3 className={style.day_info_author}>
{`${author} ${suffix}`}
</h3>
<TaskInfo tasks={tasks}/> <TaskInfo tasks={tasks}/>
</div> </div>
); );
@ -78,4 +91,9 @@ function DayInfo({ day, order }: IDayInfoProps): React.ReactElement {
); );
} }
DayInfo.defaultProps = {
events: undefined,
timestamp: undefined,
};
export default DayInfo; export default DayInfo;

View file

@ -0,0 +1,19 @@
import React from 'react';
import style from '../styles/index.module.scss';
interface IExtensionIconProps {
title: string;
}
function ExtensionIcon({
title,
}: IExtensionIconProps): React.ReactElement | null {
return (
<div className={style.extension_icon}>
{title || ''}
</div>
);
}
export default ExtensionIcon;

View file

@ -0,0 +1,28 @@
import React from 'react';
import style from '../styles/index.module.scss';
interface IExtensionLineProps {
title: string;
value: any;
}
function ExtensionLine({
title,
value,
}: IExtensionLineProps): React.ReactElement | null {
if (!value || !title) return null;
return (
<div className={style.extension_line}>
<div className={style.extension_line_title}>
{title || ''}
</div>
<div className={style.extension_line_value}>
{value || ''}
</div>
</div>
);
}
export default ExtensionLine;

View file

@ -0,0 +1,44 @@
import React from 'react';
import Icon from './components/Icon';
import Line from './components/Line';
import style from './styles/index.module.scss';
interface IExtensionProps {
statistic: any;
}
function Extension({
statistic,
}: IExtensionProps): React.ReactElement | null {
if (!statistic) return null;
const getValue = (more: any) => `${more.author} (${more.percent.toFixed(1)}%)`;
return (
<div className={style.extension}>
<Icon title={statistic.extension} />
<h6>
Чаще всего
</h6>
<Line
title="Добавляет:"
value={getValue(statistic.more.added)}
/>
<Line
title="Меняет:"
value={getValue(statistic.more.changes)}
/>
<Line
title="Удаляет:"
value={getValue(statistic.more.removed)}
/>
</div>
);
}
Extension.defaultProps = {
rows: [],
};
export default Extension;

View file

@ -0,0 +1,57 @@
@import '../../../../styles/variables';
.extension {
display: inline-block;
width: 300px;
margin: 0 0 24px 24px;
padding: 24px;
vertical-align: top;
border-radius: var(--border-radius-l);
background-color: white;
&_icon {
display: block;
width: 64px;
margin: 0 auto 24px auto;
padding: 24px 0;
font-size: var(--font-l);
font-weight: bold;
text-align: center;
text-transform: uppercase;
border-radius: 32px;
background-color: var(--color-grey);
}
&_line {
display: block;
margin: 0 0 6px 0;
padding: 0;
text-align: left;
white-space: nowrap;
&_title,
&_value {
font-weight: 100;
font-size: var(--font-xs);
display: inline-block;
padding: 0;
margin: 0;
text-align: left;
line-height: var(--font-m);
text-decoration: none;
vertical-align: top;
white-space: normal;
color: var(--color-grey);
}
&_title {
width: 100px;
}
}
}

View file

@ -0,0 +1,34 @@
import React from 'react';
import IMessage from '../interfaces/Message';
import style from '../styles/index.module.scss';
interface ICardProps {
message: IMessage,
}
function Card({ message }: ICardProps) {
const className = {
error: style.notifications_item_error,
warning: style.notifications_item_warning,
success: style.notifications_item_success,
info: style.notifications_item_info,
}[message.type || 'success'] || style.notifications_item_info;
return (
<div className={`${style.notifications_item} ${className}`}>
{message.title && (
<h6 className={style.notifications_item_title}>
{message.title}
</h6>
)}
{message.description && (
<p className={style.notifications_item_description}>
{message.description}
</p>
)}
</div>
);
}
export default Card;

View file

@ -0,0 +1,26 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { observer } from 'mobx-react-lite';
import Message from './components/Message';
import IMessage from './interfaces/Message';
import notificationsStore from './store/index';
import style from './styles/index.module.scss';
const Notifications = observer(() => {
const items = notificationsStore.messages.map((message: IMessage) => (
<Message
key={message.id}
message={message}
/>
));
return ReactDOM.createPortal((
<div className={style.notifications}>
{items}
</div>
), document.body);
});
export default Notifications;

View file

@ -0,0 +1,6 @@
export default interface IMessage {
id?: number;
title?: string;
description?: string;
type?: 'error' | 'warning' | 'success' | 'info';
}

View file

@ -0,0 +1,48 @@
import { makeObservable, observable, action } from 'mobx';
import IMessage from '../interfaces/Message';
interface INotificationsStore {
timer: any;
messages: IMessage[];
show: Function;
startClearTimer: Function;
}
class NotificationsStore implements INotificationsStore {
timer: any = null;
messages: IMessage[] = [];
constructor() {
makeObservable(this, {
messages: observable,
show: action,
startClearTimer: action,
});
}
show(message?: any) {
this.messages.push({
id: Math.random(),
title: message?.title || message || 'Изменения сохранены',
description: message?.description || '',
type: message?.type || 'success',
});
this.startClearTimer();
}
startClearTimer() {
if (this.timer) return;
this.timer = setInterval(() => {
this.messages.shift();
if (this.messages.length) return;
clearInterval(this.timer);
this.timer = null;
}, 3500);
}
}
const notificationsStore = new NotificationsStore();
export default notificationsStore;

View file

@ -0,0 +1,55 @@
@import '../../../../styles/variables';
.notifications {
position: fixed;
top: 12px;
right: 0;
display: inline-block;
&_item {
display: block;
width: 250px;
padding: 12px;
margin: 0 12px 12px;
box-sizing: border-box;
box-shadow: 2px 2px 3px var(--color-grey);
border: 1px solid var(--color-border);
background-color: #FFFFFF;
border-radius: 4px;
&_error {
background-color: var(--color-12);
}
&_warning {
background-color: var(--color-32);
}
&_success {
background-color: var(--color-13);
}
&_info {
background-color: #F1F4FC;
}
&_title,
&_description {
font-size: var(--font-s);
padding: 0;
text-align: left;
color: var(--color-black);
}
&_title {
font-weight: bold;
margin: 0;
}
&_description {
font-weight: 100;
margin: 0 0 4px 0;
}
}
}

View file

@ -0,0 +1,26 @@
import React from 'react';
import style from './index.module.scss';
interface IInfoProps {
title: string;
duration: number;
}
function Info({
title,
duration,
}: IInfoProps): React.ReactElement | null {
return (
<div
className={style.races_track_info}
style={{
animationDelay: `${duration + 1}s`,
}}
>
{title}
</div>
);
}
export default Info;

View file

@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import { shuffle } from './index';
import Info from './Info';
import style from './index.module.scss';
const DURATION = {
MIN: 5,
BASE: 10,
};
const animations = [
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'linear',
'cubic-bezier(0.1, 0.7, 1, 0.1)',
];
function getRandom(max: number) {
return Math.floor(Math.random() * (max - 0 + 1)) + 0;
}
interface ITrackProps {
title: string;
speed: number;
type?: string;
canStart?: boolean;
}
function Track({
title,
speed,
type,
canStart,
}: ITrackProps): React.ReactElement | null {
const modeIndex = getRandom(animations.length - 1);
const [mode] = useState<string>(animations[modeIndex]);
useEffect(() => {
shuffle(animations);
}, []);
if (!title) return null;
const duration = DURATION.MIN + (DURATION.BASE * (1 - speed)) * 3;
const classForMove = canStart ? style.races_track_animation : '';
return (
<div className={`${style.races_track} ${type || ''}`}>
{canStart && (
<Info
title={title}
duration={duration}
/>
)}
<div
className={`${style.races_track_car} ${type || ''} ${classForMove || ''}`}
style={{
animationTimingFunction: mode,
animationDuration: `${duration}s`,
}}
>
<div className={`${style.races_track_car_title} ${type || ''}`}>
{title}
</div>
</div>
</div>
);
}
Track.defaultProps = {
type: '',
canStart: false,
};
export default Track;

View file

@ -0,0 +1,106 @@
@import '../../../styles/variables';
.races {
position: relative;
margin: 0 auto var(--space-xxl);
border: var(--space-xs) solid var(--color-border);
border-left: none;
border-right: none;
&_track {
position: relative;
height: 70px;
padding: var(--space-xxs);
margin: 0 auto;
text-align: right;
border-bottom: 1px solid var(--color-border);
background-color: var(--color-grey);
&_car {
position: absolute;
top: 0;
left: 0;
width: 10%;
height: 100%;
background-image: url('../../../assets/games/car.png');
background-position: center center;
background-repeat: no-repeat;
background-size: auto 70%;
&_title {
font-size: 12px;
display: inline-block;
width: 100%;
margin: 0;
padding: var(--space-xss);
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
border: 1px solid var(--color-grey);
border-radius: var(--border-radius-s);
color: var(--color-black);
background-color: var(--color-border);
}
}
&_info {
font-size: 12px;
display: inline-block;
width: 10%;
height: 100%;
margin: 0 12%;
padding: var(--space-m);
text-align: left;
border-radius: var(--border-radius-l);
border: 1px solid var(--color-grey);
color: var(--color-black);
background-color: var(--color-border);
animation-name: races_track_info;
animation-iteration-count: 1;
animation-duration: 1s;
animation-direction: alternate;
animation-fill-mode: both;
}
&_animation {
animation-name: races_track_car;
animation-iteration-count: 1;
animation-direction: alternate;
animation-fill-mode: both;
}
}
&_button {
position: absolute;
top: var(--space-l);
left: calc(50% - 100px);
z-index: 1;
}
}
@keyframes races_track_car {
from {
left: 0;
}
to {
left: 90%;
}
}
@keyframes races_track_info {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View file

@ -0,0 +1,57 @@
import React, { useState } from 'react';
import UiKitButton from 'ts/components/UiKit/components/Button';
import Track from './Track';
import style from './index.module.scss';
export function shuffle(items: any[]) {
// @ts-ignore
for (let j, x, i = items.length; i; j = parseInt(Math.random() * i), x = items[--i], items[i] = items[j], items[j] = x) {}
return items;
}
interface IRacesProps {
tracks: {
title: string,
speed: number,
type?: string,
}[];
}
function Races({
tracks,
}: IRacesProps): React.ReactElement | null {
const [showAnimation, setShowAnimation] = useState<boolean>(false);
if (!tracks.length) return null;
const lines = shuffle(tracks).map((track: any) => {
return (
<Track
key={track.title}
title={track.title}
speed={track.speed}
canStart={showAnimation}
/>
);
});
return (
<div className={style.races}>
{!showAnimation && (
<UiKitButton
className={style.races_button}
onClick={() => {
setShowAnimation(true);
}}
>
Поехали
</UiKitButton>
)}
{lines}
</div>
);
}
export default Races;

View file

@ -0,0 +1,62 @@
@import '../../../styles/variables';
.splash_screen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-color: var(--color-black);
animation: splash_screen .5s linear 5.5s forwards;
&_container {
display: block;
width: 300px;
box-sizing: border-box;
text-align: center;
animation: splash_screen_container 1s linear 4s forwards;
}
&_description {
font-size: 10px;
font-weight: 100;
display: block;
padding: 0;
margin: 8px 0 0 0;
overflow: hidden;
text-overflow: ellipsis;
text-transform: none;
color: #404148;
}
}
@keyframes splash_screen {
from {
bottom: 0;
left: 0;
opacity: 1;
}
to {
bottom: 100%;
left: 100%;
opacity: 0;
}
}
@keyframes splash_screen_container {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View file

@ -0,0 +1,56 @@
import React, { useEffect, useState } from 'react';
import Logo from 'ts/pages/PageWrapper/components/sidebar/Logo';
import style from './index.module.scss';
import progress from './progress.module.scss';
const TEXT: string[] = [
'обработка файлов',
'обработка коммитов',
'нормализация данных',
'анализ времени',
'анализ состава команды',
'оценка стоимости проекта',
'оценка затрат на разработку',
'расчёт общих рекомендаций',
'расчёт частных рекомендаций',
'аудит суммарных затрат',
'расчёт персональных ачивок',
'анализ эффективности',
].reverse();
function SplashScreen(): React.ReactElement | null {
const [timer, setTimer] = useState<any>(null);
const [index, setIndex] = useState<number>(0);
useEffect(() => {
if (timer) return;
let localIndex = 0;
setTimer(setInterval(() => {
localIndex = localIndex === 0
? TEXT.length - 1
: localIndex - 1;
setIndex(localIndex);
}, 200));
return () => {
clearInterval(timer);
};
}, []);
return (
<div className={style.splash_screen}>
<div className={style.splash_screen_container}>
<Logo />
<div className={progress.progress_bar}></div>
<p className={style.splash_screen_description}>
{TEXT[index] || ''}
</p>
</div>
</div>
);
}
export default SplashScreen;

View file

@ -0,0 +1,69 @@
.progress_bar {
display: block;
width: 100%;
height: 16px;
text-align: left;
border-radius: 4px;
background-color: #404148;
&:after {
content: '';
font-size: 0.8em;
display: block;
width: 0;
height: 100%;
padding-top: 0.1em;
text-align: right;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: break-word;
border-radius: 4px;
color: white;
background-color: green;
background-size: 23px 22px;
background-image: -webkit-repeating-linear-gradient(135deg, rgba(255, 255, 255, .35), rgba(255, 255, 255, .35) 8px, rgba(255, 255, 255, 0) 9px, rgba(255, 255, 255, 0) 16px);
background-image: repeating-linear-gradient(-45deg, rgba(255, 255, 255, .35), rgba(255, 255, 255, .35) 8px, rgba(255, 255, 255, 0) 9px, rgba(255, 255, 255, 0) 16px);
}
}
.progress_bar,
.progress_bar:after {
transition: background-color 0.7s, width 0.5s;
}
.progress_bar:after {
animation: progress_bar 4.3s linear forwards;
}
@keyframes progress_bar {
from {
width: 0;
background-size: 23px 22px;
}
10% {
width: 1%;
background-size: 23px 32px;
}
80% {
width: 50%;
background-size: 23px 32px;
}
90% {
width: 90%;
background-size: 23px 32px;
}
98% {
width: 92%;
background-size: 23px 32px;
}
to {
width: 100%;
background-size: 23px 44px;
}
}

View file

@ -0,0 +1,68 @@
import React from 'react';
import { IColumn } from '../interfaces/Column';
import style from '../styles/index.module.scss';
import DefaultCell from './cells/CellDefault';
interface IBodyProps {
rows: any[];
columns: IColumn[];
disabledRow?: (row: any) => boolean;
className?: string;
}
function Body({
rows,
disabledRow,
columns,
className,
}: IBodyProps) {
const formattedRows = rows?.map((row: any, index: number) => {
const cells = columns.map((column: IColumn, columnIndex: number) => {
const value = column.properties
? row[column.properties]
: row;
const formattedValue = column.formatter
? column.formatter(value)
: value;
const content: any = typeof column.template === 'function'
? column.template(formattedValue)
: `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`;
return (
<DefaultCell
key={`${column.title}_${columnIndex}`}
column={column}
row={row}
>
{content}
</DefaultCell>
);
});
const rowClassName = disabledRow && disabledRow(row)
? style.disabled
: '';
return (
<div
key={index}
className={`${style.table_row} ${rowClassName} ${className}`}
>
{cells}
</div>
);
});
return (
<>
{formattedRows}
</>
);
}
Body.defaultProps = {
className: '',
};
export default Body;

View file

@ -0,0 +1,61 @@
import React from 'react';
import { IColumn } from '../interfaces/Column';
function Column({
template,
title,
properties,
prefixes,
suffixes,
formatter,
className,
style,
isFixed,
isSortable,
isResizable,
isDraggable,
isShow,
width,
onClick,
}: IColumn): JSX.Element {
return (
<>
{{
template,
title,
properties,
prefixes,
suffixes,
formatter,
className,
style,
isFixed,
isSortable,
isResizable,
isDraggable,
isShow,
width,
onClick,
}}
</>
);
}
Column.defaultProps = {
title: '',
prefixes: [''],
suffixes: [''],
formatter: (value: any) => value,
className: '',
isDisabled: false,
isFixed: false,
isSortable: false,
isResizable: false,
isDraggable: false,
isShow: true,
width: undefined,
onClick: undefined,
};
export default Column;

View file

@ -0,0 +1,64 @@
import React from 'react';
import localization from 'ts/helpers/Localization';
import { IColumn } from '../interfaces/Column';
import headerStyle from '../styles/header.module.scss';
import style from '../styles/index.module.scss';
interface ITitleProps {
columns: IColumn[];
className?: string;
updateSort?: Function;
}
function Header({
columns,
className,
updateSort,
}: ITitleProps) {
const cells = columns.map((column: IColumn, columnIndex: number) => {
const columnClassName = typeof column.className === 'function'
? column.className('header', columnIndex)
: column.className;
return (
<div
key={`${column.title}_${columnIndex}`}
className={`${style.table_header_cell} ${className} ${columnClassName || ''}`}
style={{ width: column.width }}
>
<span
onClick={() => {
if (!column.isSortable || !updateSort) return;
updateSort([{
property: typeof column.isSortable === 'string' ? column.isSortable : column.properties,
direction: [1, -1][column.sortDirection || 0] || 0,
}]);
}}
>
{localization.get(column.title)}
</span>
{column.title && column.sortDirection === -1 && (
<div className={headerStyle.sort_down} />
)}
{column.title && column.sortDirection === 1 && (
<div className={headerStyle.sort_up} />
)}
</div>
);
});
return (
<div className={`${style.table_row} ${className}`}>
{cells}
</div>
);
}
Header.defaultProps = {
className: '',
updateSort: () => {},
};
export default Header;

View file

@ -0,0 +1,45 @@
import React, { ReactNode } from 'react';
import { IColumn } from '../../interfaces/Column';
import style from '../../styles/index.module.scss';
interface IDefaultCellProps {
column: IColumn,
row: any,
className?: string,
children?: ReactNode | string | number | boolean | null;
}
function DefaultCell({
column,
row,
className,
children,
}: IDefaultCellProps): JSX.Element {
const columnClassName = typeof column.className === 'function'
? column.className('body', row)
: column.className;
const onClick = column.onClick
? (() => { if (column.onClick) column.onClick(row); })
: undefined;
return (
<div
key={column.title}
className={`${style.table_cell} ${className || ''} ${columnClassName || ''}`}
style={{
width: column.width,
cursor: onClick ? 'pointer' : 'auto',
}} // @ts-ignore
onClick={onClick}
>
{children}
</div>
);
}
DefaultCell.defaultPeops = {
className: '',
};
export default DefaultCell;

View file

@ -0,0 +1,30 @@
import ISort from 'ts/interfaces/Sort';
import { IColumn } from '../interfaces/Column';
function getColumnConfigs(
dirtyColumns: IColumn[] = [],
defaultWidth?: number,
sort?: ISort[],
): IColumn[] {
const sortByColumns = sort?.reduce((ref: any, item: ISort) => {
ref[item.property] = item.direction;
return ref;
}, {});
const columns: IColumn[] = dirtyColumns.map((column: IColumn) => ({
...column,
sortDirection: typeof column?.isSortable === 'string'
? (sortByColumns[column?.isSortable || ''] || 0)
: (sortByColumns[column?.properties || ''] || 0),
width: column.userWidth || column.defaultWidth || defaultWidth || column.width || 150,
}));
const middle = Math.floor(columns.length / 2);
return [
...columns.filter((column: IColumn, index: number) => column.isFixed && index <= middle),
...columns.filter((column: IColumn) => !column.isFixed),
...columns.filter((column: IColumn, index: number) => column.isFixed && index > middle),
];
}
export default getColumnConfigs;

View file

@ -0,0 +1,20 @@
import type { IColumn } from '../interfaces/Column';
export default function getDefaultColumnWidth(
columns: IColumn[],
tableRef: any,
): number {
if (!tableRef?.current?.offsetWidth) return 150;
const visibleColumns = columns.filter(({ isShow }: IColumn) => isShow);
const columnsWidth = visibleColumns.map((column: IColumn) => (
column.userWidth || column.defaultWidth || 0
));
const fixedWidth = columnsWidth.reduce((sum: number, width: number) => sum + width, 0);
const adaptiveColumnsCount = columnsWidth.filter((width: number) => !width).length;
const tableWidth = tableRef?.current?.offsetWidth - fixedWidth;
const adaptiveColumnsWidth = tableWidth / adaptiveColumnsCount;
return Math.max(adaptiveColumnsWidth, 40);
}

View file

@ -0,0 +1,41 @@
import React from 'react';
import { ColumnTypesEnum } from '../interfaces/Column';
import style from '../styles/index.module.scss';
export default function getDefaultProps(children: React.ReactNode) {
return React.Children.map(children, (child: React.ReactNode) => {
if (!React.isValidElement(child)) return null;
const template = child?.props?.template || ColumnTypesEnum.STRING;
// @ts-ignore
const className = child?.props?.className || {
[ColumnTypesEnum.STRING]: '',
[ColumnTypesEnum.NUMBER]: style.table_cell_number,
[ColumnTypesEnum.SHORT_NUMBER]: style.table_cell_number,
}[template || ''] || '';
// @ts-ignore
const defaultWidth = child?.props?.width || {
[ColumnTypesEnum.STRING]: 200,
[ColumnTypesEnum.NUMBER]: 110,
[ColumnTypesEnum.SHORT_NUMBER]: 70,
}[template || ''] || 0;
// @ts-ignore
const isSortable = child?.props?.isSortable // @ts-ignore
? child?.props?.isSortable
: [ColumnTypesEnum.STRING, ColumnTypesEnum.NUMBER, ColumnTypesEnum.SHORT_NUMBER].includes(template);
return {
...child.props as object,
className,
template,
isSortable,
width: undefined,
userWidth: undefined,
defaultWidth,
};
});
}

View file

@ -0,0 +1,63 @@
import React from 'react';
import ISort from 'ts/interfaces/Sort';
import { IColumn } from './interfaces/Column';
import Header from './components/Header';
import Body from './components/Body';
import getDefaultColumnWidth from './helpers/getDefaultColumnWidth';
import getColumnConfigs from './helpers/getColumnConfigs';
import getDefaultProps from './helpers/getDefaultProps';
import style from './styles/index.module.scss';
interface ITableProps {
rows: any[];
sort?: ISort[];
disabledRow?: (row: any) => boolean;
updateSort?: Function,
children: React.ReactNode | React.ReactNode[];
}
function Table({
rows = [],
sort = [],
disabledRow,
updateSort,
children,
}: ITableProps): React.ReactElement | null {
if (!rows || !rows.length) return null;
const refTable = React.useRef() as React.MutableRefObject<HTMLDivElement>;
const defaultColumns = getDefaultProps(children) as IColumn[];
const defaultWidth = getDefaultColumnWidth(defaultColumns, refTable);
const columns = getColumnConfigs(defaultColumns, defaultWidth, sort);
return (
<div className={`${style.table_wrapper}`}>
<div
ref={refTable}
className={`${style.table}`}
>
<Header
columns={columns}
updateSort={updateSort}
/>
<Body
rows={rows}
columns={columns}
disabledRow={disabledRow}
/>
</div>
</div>
);
}
Table.defaultProps = {
rows: [],
sort: [],
updateSort: () => {},
};
export default Table;

View file

@ -0,0 +1,49 @@
export type ColumnType = 'STRING' | 'NUMBER' | 'SHORT_NUMBER';
/** Тип столбца определяет тип содержимого всех ячеек столбца */
export enum ColumnTypesEnum {
STRING = 'STRING',
NUMBER = 'NUMBER',
SHORT_NUMBER = 'SHORT_NUMBER',
}
export interface IColumn {
/** Тип столбца */
template?: ColumnTypesEnum | Function,
/** Уникальный ключ столбца */
properties?: string,
/** Заголовок столбца */
title?: string,
/** Префиксы для заголовка столбца */
prefixes?: string,
/** Суффиксы для заголовка столбца (%, $ и т.д.) */
suffixes?: string,
/** Функция для форматирования данных в столбце */
formatter?: Function,
/** Направление сортировки */
sortDirection?: number,
/** Фиксированный столбец */
isFixed?: boolean,
/** Сортировка столбца */
isSortable?: boolean | string,
/** Изменение ширины столбца */
isResizable?: boolean,
/** Drag-and-Drop столбца */
isDraggable?: boolean,
/** Видимость столбца */
isShow?: boolean,
/** Клас для колонки */
className?: string | Function
/** Стилья для колонки */
style?: Function,
/** Ширина столбца заданная в верстке */
defaultWidth?: number,
/** Ширина столбца установленная пользователем */
userWidth?: number,
/** Ширина столбца итоговая */
width?: number,
/** Клик на ячейку */
onClick?: Function,
}

View file

@ -0,0 +1,28 @@
@import '../../../../styles/variables';
.title {
font-size: var(--font-l);
font-weight: 100;
margin: 24px 0;
color: var(--color-black);
}
.sort_up,
.sort_down {
display: inline-block;
height: 0;
width: 0;
margin: 0 0 -5px 8px;
cursor: pointer;
transform: rotateZ(-45deg);
border: 6px solid var(--color-grey);
border-left-color: white;
border-bottom-color: white;
}
.sort_down {
margin: 0 0 3px 8px;
transform: rotateZ(135deg);
}

View file

@ -0,0 +1,92 @@
@import '../../../../styles/variables';
.table_wrapper {
display: block;
overflow-x: auto;
&::-webkit-scrollbar {
height: 8px;
background-color: #DDDDDD;
}
&::-webkit-scrollbar-thumb {
background: #AAAAAA;
}
}
.table {
display: block;
width: fit-content;
--table-cell-height: 48px;
--table-bar-width: 350px;
}
.table_tree {
--table-cell-height: 22px;
--table-bar-width: 200px;
}
.table_row {
position: relative;
font-weight: 100;
display: block;
white-space: nowrap;
border-bottom: 1px solid #EEEEEE;
}
.table_row:last-child {
border-bottom: none;
}
.table_row_hide {
opacity: 0.2;
}
.table_cell,
.table_header_cell {
font-size: var(--font-xs);
z-index: 0;
display: inline-block;
height: var(--table-cell-height);
line-height: var(--table-cell-height);
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
}
.table_cell {
padding: 0 4px;
}
.table_header_cell {
font-weight: bold;
height: var(--table-cell-height);
padding: 0 4px;
line-height: var(--table-cell-height);
background-color: #FFFFFF;
}
.table_cell:first-child,
.table_header_cell:first-child {
position: sticky;
top: 0;
left: 0;
z-index: 1;
}
.table_cell:first-child {
background-color: rgba(255, 255, 255, 0.9);
}
.table_cell_number {
text-align: right;
}
.disabled {
opacity: 0.4;
filter: grayscale(0.6);
}

View file

@ -0,0 +1,25 @@
import React, { useState } from 'react';
import style from '../styles/index.module.scss';
interface ITitleProps {
title: string,
}
function Title({ title }: ITitleProps): React.ReactElement | null {
const [show, setShow] = useState<boolean>(false);
return (
<div className={`${style.tv100and1_cell_title}`}>
{title}
<button
className={`${style.tv100and1_button} ${show ? style.animation : ''}`}
onClick={() => {
setShow(true);
}}
/>
</div>
);
}
export default Title;

View file

@ -0,0 +1,51 @@
import React from 'react';
import LineChart from 'ts/components/LineChart';
import getOptions from 'ts/components/LineChart/helpers/getOptions';
import Title from './components/Title';
import style from './styles/index.module.scss';
interface ITv100And1Props {
rows: {
title: string,
value: number,
}[];
}
function Tv100And1({
rows = [],
}: ITv100And1Props): React.ReactElement | null {
if (!rows || !rows.length) return null;
const chartOptions = getOptions({ max: rows[0].value, suffix: 'сиволов' });
const formattedRows = rows.map((row: any) => (
<div
key={row.title}
className={`${style.tv100and1_row}`}
>
<Title title={row.title} />
<div className={`${style.tv100and1_cell_value}`}>
{row.value}
</div>
<div className={`${style.tv100and1_cell_chart}`}>
<LineChart
options={chartOptions}
value={row.value}
/>
</div>
</div>
));
return (
<div className={`${style.tv100and1}`}>
{formattedRows}
</div>
);
}
Tv100And1.defaultProps = {
rows: [],
};
export default Tv100And1;

View file

@ -0,0 +1,90 @@
@import '../../../../styles/variables';
.tv100and1 {
&_row {
display: block;
white-space: nowrap;
}
&_cell_title,
&_cell_value,
&_cell_chart {
position: relative;
font-size: var(--font-xs);
display: inline-block;
padding: var(--space-m) 0;
line-height: var(--table-cell-height);
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
text-align: left;
}
&_cell_title {
width: 250px;
}
&_cell_value {
width: 50px;
text-align: right;
}
&_cell_chart {
width: calc(100% - 300px);
padding: 0;
}
&_button {
position: absolute;
left: 0;
right: 0;
bottom: 2px;
display: block;
height: calc(100% - 4px);
transform: rotate3d(1, 0, 0, 0);
box-sizing: border-box;
border: none;
cursor: pointer;
background-color: var(--color-grey);
background-image: url('../../../../assets/games/tv100and1.png');
background-repeat: repeat-x;
background-size: auto 100%;
&.animation {
animation-name: tv_100_and_1;
animation-iteration-count: 1;
animation-duration: 1s;
animation-direction: alternate;
animation-timing-function: ease-in;
animation-fill-mode: both;
}
}
}
@keyframes tv_100_and_1 {
from {
transform: rotate3d(1, 0, 0, 0);
}
10% {
transform: rotate3d(1, 0, 0, 90deg);
}
25% {
transform: rotate3d(1, 0, 0, 0);
}
55% {
transform: rotate3d(1, 0, 0, 90deg);
}
80% {
transform: rotate3d(1, 0, 0, 0);
}
to {
transform: rotate3d(1, 0, 0, 90deg);
}
}

View file

@ -1,47 +1,11 @@
import React from 'react'; import React from 'react';
import dataGripStore from 'ts/store/DataGrip';
import Day from './Day';
import IMonth from '../interfaces/Month'; import IMonth from '../interfaces/Month';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
import { getEvents } from '../helpers/day';
function getPercentByMax(countCommit: number, max: number) {
const value = ((countCommit || 0) * 100) / max;
return (value - value % 1) / 100;
}
function getIconUrl(month: IMonth, dayInMonth: number) {
const addPerson = month.firstDay?.[dayInMonth];
const removePerson = month.lastDay?.[dayInMonth];
if (addPerson && removePerson) return './assets/chart/commit.svg';
if (removePerson) return './assets/chart/commit.svg';
if (addPerson) return './assets/chart/commit.svg';
return '';
}
function getColor(isWeekend: boolean, opacity: number): string {
const colors = isWeekend ? [
'#ED675F', // 1
'#EB817C', // 0.8
'#E98E8A', // 0.7
'#E89B99', // 0.6
'#E7A8A7', // 0.5
'#E7B5B6', // 0.4
'#E6C3C4', // 0.3
'#E4CFD3', // 0.2
] : [
'#4162B5', // 0 1
'#617DC1', // 1 0.8
'#718AC6', // 2 0.7
'#8198CD', // 3 0.6
'#91A6D2', // 4 0.5
'#A2B3D8', // 5 0.4
'#B2C1DE', // 6 0.3
'#C2CEE4', // 7 0.2
];
if (opacity >= 0.8) return colors[1];
if (opacity >= 0.6) return colors[3];
if (opacity >= 0.4) return colors[5];
return colors[7];
}
interface IBodyProps { interface IBodyProps {
month: IMonth; month: IMonth;
@ -53,30 +17,26 @@ function Body({
maxCommits, maxCommits,
}: IBodyProps): React.ReactElement | null { }: IBodyProps): React.ReactElement | null {
const firstDay = month.date.getDay() - 1; const firstDay = month.date.getDay() - 1;
const weekend = [5, 6, 12, 13, 19, 20, 26, 27, 33, 34, 40, 41];
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const lastDay = firstDay + daysInMonth[month.month]; const lastDay = firstDay + daysInMonth[month.month];
const allDays = (new Array(6 * 7)).fill(0); const allDays = (new Array(6 * 7)).fill(0);
let currentDay = 0; let currentDay = 0;
const events = getEvents(dataGripStore);
const days = allDays.map((v: any, index: number) => { const days = allDays.map((v: any, index: number) => {
const dayInfo = month.commits[currentDay]; const dayInfo = month.commits[currentDay];
if (dayInfo?.dayInMonth === (index - firstDay + 1)) { if (dayInfo?.dayInMonth === (index - firstDay + 1)) {
currentDay += 1; currentDay += 1;
const opacity = getPercentByMax(dayInfo.commits, maxCommits);
const isWeekend = weekend.includes(index);
const backgroundColor = getColor(isWeekend, opacity);
const iconUrl = getIconUrl(month, dayInfo.dayInMonth);
return ( return (
<div <Day
key={index} key={index}
className={style.year_chart_month_body_day} month={month}
style={{ maxCommits={maxCommits}
backgroundColor, dayNumber={index}
backgroundImage: iconUrl ? `url(${iconUrl})` : '', dayInfo={dayInfo}
}} events={events}
/> />
); );
} }

View file

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import DayInfo from 'ts/components/DayInfo';
import dataGripStore from 'ts/store/DataGrip';
import IHashMap from 'ts/interfaces/HashMap';
import IMonth from '../interfaces/Month';
import style from '../styles/index.module.scss';
import {
getPercentByMax,
getColor,
getIconUrl, getDayText,
} from '../helpers/day';
interface IDayProps {
maxCommits: number;
dayNumber: any;
month: IMonth;
dayInfo: any;
events: IHashMap<any>;
}
function Day({
month,
dayInfo,
maxCommits,
dayNumber,
events,
}: IDayProps): React.ReactElement | null {
const [showInfo, setShowInfo] = useState<boolean>(false);
const weekend = [5, 6, 12, 13, 19, 20, 26, 27, 33, 34, 40, 41];
const opacity = getPercentByMax(dayInfo.commits, maxCommits);
const isWeekend = weekend.includes(dayNumber);
const backgroundColor = getColor(isWeekend, opacity);
const iconUrl = getIconUrl(month, dayInfo.dayInMonth);
const text = getDayText(events, dayInfo.timestamp);
return ( // @ts-ignore
<div
className={style.year_chart_month_body_day}
title={`коммитов: ${dayInfo.commits}, задач: ${dayInfo.tasksInDay || 0}`}
style={{
backgroundColor,
backgroundImage: iconUrl ? `url(${iconUrl})` : '',
}}
onClick={() => {
setShowInfo(!showInfo);
}}
>
{showInfo ? (
<>
{'📌'}
<div className={style.year_chart_month_body_day_arrow} />
<div className={style.year_chart_month_body_day_info}>
<DayInfo // @ts-ignore
day={dayInfo}
events={events}
timestamp={dayInfo.timestamp}
order={dataGripStore.dataGrip.author.list}
/>
</div>
</>
) : text}
</div>
);
}
Day.defaultProps = {
rows: [],
};
export default Day;

View file

@ -19,9 +19,7 @@ function Month({
}: IMonthProps): React.ReactElement | null { }: IMonthProps): React.ReactElement | null {
return ( return (
<div className={`${style.year_chart_month}`}> <div className={`${style.year_chart_month}`}>
<Header <Header month={month} />
month={month}
/>
<Body <Body
month={month} month={month}
maxCommits={maxCommits} maxCommits={maxCommits}

View file

@ -0,0 +1,70 @@
import IHashMap from 'ts/interfaces/HashMap';
import IMonth from '../interfaces/Month';
export function getPercentByMax(countCommit: number, max: number) {
const value = ((countCommit || 0) * 100) / max;
return (value - value % 1) / 100;
}
export function getIconUrl(month: IMonth, dayInMonth: number) {
const addPerson = month.firstDay?.[dayInMonth];
const removePerson = month.lastDay?.[dayInMonth];
if (addPerson && removePerson) return './assets/chart/commit.svg';
if (removePerson) return './assets/chart/commit.svg';
if (addPerson) return './assets/chart/commit.svg';
return '';
}
export function getColor(isWeekend: boolean, opacity: number): string {
const colors = isWeekend ? [
'#ED675F', // 1
'#EB817C', // 0.8
'#E98E8A', // 0.7
'#E89B99', // 0.6
'#E7A8A7', // 0.5
'#E7B5B6', // 0.4
'#E6C3C4', // 0.3
'#E4CFD3', // 0.2
] : [
'#4162B5', // 0 1
'#617DC1', // 1 0.8
'#718AC6', // 2 0.7
'#8198CD', // 3 0.6
'#91A6D2', // 4 0.5
'#A2B3D8', // 5 0.4
'#B2C1DE', // 6 0.3
'#C2CEE4', // 7 0.2
];
if (opacity >= 0.8) return colors[1];
if (opacity >= 0.6) return colors[3];
if (opacity >= 0.4) return colors[5];
return colors[7];
}
export function getDayText(events: IHashMap<any>, timestamp: string): string {
const addEmployees = events?.firstCommit?.[timestamp];
const removeEmployees = events?.lastCommit?.[timestamp];
if (addEmployees && removeEmployees) return '+-';
if (removeEmployees) return '-';
if (addEmployees) return '+';
return '';
}
function getRefAuthorByTime(list: any[], property: string) {
return list.reduce((refTimeAuthor: any, item: any) => {
if (item.isStaff) return refTimeAuthor;
const key = item?.[property]?.timestamp;
if (!refTimeAuthor[key]) refTimeAuthor[key] = [];
refTimeAuthor[key].push(item.author);
return refTimeAuthor;
}, {});
}
export function getEvents(dataGripStore: any) {
const list = dataGripStore.dataGrip.author.statistic;
return {
firstCommit: getRefAuthorByTime(list, 'firstCommit'),
lastCommit: getRefAuthorByTime(list, 'lastCommit'),
};
}

View file

@ -3,5 +3,7 @@ export default interface IWorkDay {
year: number; year: number;
day: number; day: number;
dayInMonth: number; dayInMonth: number;
tasksInDay: number;
timestamp: string;
commits: number; commits: number;
} }

View file

@ -44,13 +44,64 @@
max-width: var(--month-size); max-width: var(--month-size);
&_day { &_day {
position: relative;
display: inline-block; display: inline-block;
width: var(--day-size); width: var(--day-size);
height: var(--day-size); height: var(--day-size);
margin: 0 1px 1px 0; margin: 0 1px 1px 0;
vertical-align: top; vertical-align: top;
background-color: var(--color-border); background-color: var(--color-border);
background-blend-mode: screen; background-blend-mode: screen;
cursor: pointer;
text-align: center;
&_arrow {
position: absolute;
top: 20px;
left: 0;
z-index: 1;
display: inline-block;
width: 0;
height: 0;
transform: rotateZ(-45deg);
border: 16px solid white;
border-right: none;
border-bottom: none;
background-color: white;
}
&_info {
font-size: var(--font-s);
position: absolute;
left: -175px;
top: var(--space-xxl);
z-index: 1;
display: block;
width: 350px;
max-height: 350px;
overflow-y: scroll;
overflow-x: hidden;
padding: var(--space-s);
cursor: default;
text-align: left;
border-radius: var(--border-radius-m);
box-shadow: 2px 2px 5px var(--color-border);
border: 1px solid var(--color-border);
background-color: white;
&::-webkit-scrollbar {
width: 8px;
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background: #AAAAAA;
}
}
} }
} }
} }

View file

@ -40,6 +40,9 @@ export default class DataGripByAuthor {
statistic.hours.push(commit.hours); statistic.hours.push(commit.hours);
statistic.messageLength.push(commit.message.length); statistic.messageLength.push(commit.message.length);
statistic.totalMessageLength += commit.message.length || 0; statistic.totalMessageLength += commit.message.length || 0;
statistic.maxMessageLength = commit.message.length > statistic.maxMessageLength
? commit.message.length
: statistic.maxMessageLength;
statistic.commitsByDayAndHour[commit.day][commit.hours] += 1; statistic.commitsByDayAndHour[commit.day][commit.hours] += 1;
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics); statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
} }
@ -60,6 +63,7 @@ export default class DataGripByAuthor {
commitsByDayAndHour, commitsByDayAndHour,
messageLength: [commit.message.length || 0], messageLength: [commit.message.length || 0],
totalMessageLength: commit.message.length || 0, totalMessageLength: commit.message.length || 0,
maxMessageLength: commit.message.length || 0,
wordStatistics: DataGripByAuthor.#updateWordStatistics(commit), wordStatistics: DataGripByAuthor.#updateWordStatistics(commit),
}; };
} }
@ -150,6 +154,7 @@ export default class DataGripByAuthor {
isStaff, isStaff,
middleMessageLength, middleMessageLength,
maxMessageLength: dot.maxMessageLength,
commitsByDayAndHourTotal: DataGripByAuthor.getTotalCommitsByDayAndHour(dot.commitsByDayAndHour), commitsByDayAndHourTotal: DataGripByAuthor.getTotalCommitsByDayAndHour(dot.commitsByDayAndHour),
wordStatistics, wordStatistics,
}; };

View file

@ -0,0 +1,88 @@
import IHashMap from 'ts/interfaces/HashMap';
import MinMaxCounter from './counter';
const IGNORE_LIST = [
'.eslintrc',
'.gitignore',
'package.json',
'package-lock.json',
'tsconfig.json',
];
export default class DataGripByExtension {
statistic: any = [];
statisticByName: IHashMap<any> = {};
clear() {
this.statistic = [];
this.statisticByName = {};
}
updateTotalInfo(fileList: any[], byAuthor: any) {
const byExtension = {};
fileList.forEach((file: any) => {
if (!file.extension
|| IGNORE_LIST.includes(file.name)) return;
if (!byExtension[file.extension]) {
byExtension[file.extension] = {
extension: file.extension, authors: {}, more: {}, total: { added: 0, changes: 0, removed: 0, total: 0 },
};
}
for (let author in file.authors) {
if (!author
|| byAuthor.statisticByName[author]?.isStaff) return;
byExtension[file.extension].authors[author] = byExtension[file.extension].authors[author]
|| { added: 0, changes: 0, removed: 0 };
const statistic = file.authors[author];
const total = byExtension[file.extension].authors[author];
total.added += statistic.added;
total.changes += statistic.changes;
total.removed += statistic.removed;
byExtension[file.extension].total.added += statistic.added;
byExtension[file.extension].total.changes += statistic.changes;
byExtension[file.extension].total.removed += statistic.removed;
byExtension[file.extension].total.total += statistic.added + statistic.changes + statistic.removed;
}
});
this.#addMorePercent(byExtension);
this.statistic = Object.entries(byExtension)
.sort((a: any, b: any) => b[1].total.total - a[1].total.total)
.map((item: any) => item[1]);
this.statisticByName = byExtension;
}
#addMorePercent(byExtension: any) {
for (let extension in byExtension) {
const moreAdded = new MinMaxCounter();
const moreChanges = new MinMaxCounter();
const moreRemoved = new MinMaxCounter();
for (let author in byExtension[extension].authors) {
const statistic = byExtension[extension].authors[author];
const total = statistic.added + statistic.changes + statistic.removed;
statistic.addedPercent = (statistic.added * 100) / total;
statistic.changesPercent = (statistic.changes * 100) / total;
statistic.removedPercent = (statistic.removed * 100) / total;
moreAdded.update(statistic.addedPercent, author);
moreChanges.update(statistic.changesPercent, author);
moreRemoved.update(statistic.removedPercent, author);
}
byExtension[extension].more = {
added: { percent: moreAdded.max, author: moreAdded.maxData },
changes: { percent: moreChanges.max, author: moreChanges.maxData },
removed: { percent: moreRemoved.max, author: moreRemoved.maxData },
};
}
}
}

View file

@ -9,6 +9,7 @@ import DataGripByType from './components/type';
import DataGripByTimestamp from './components/timestamp'; 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';
class DataGrip { class DataGrip {
firstLastCommit: any = new MinMaxCounter(); firstLastCommit: any = new MinMaxCounter();
@ -27,6 +28,8 @@ class DataGrip {
recommendations: any = new Recommendations(); recommendations: any = new Recommendations();
extension: any = new DataGripByExtension();
initializationInfo: any = {}; initializationInfo: any = {};
clear() { clear() {
@ -38,6 +41,7 @@ class DataGrip {
this.timestamp.clear(); this.timestamp.clear();
this.week.clear(); this.week.clear();
this.recommendations.clear(); this.recommendations.clear();
this.extension.clear();
} }
addCommit(commit: ICommit) { addCommit(commit: ICommit) {
@ -80,6 +84,10 @@ class DataGrip {
}); });
this.#updateTotalInfo(); this.#updateTotalInfo();
} }
updateByFiles(fileList: any[]) {
this.extension.updateTotalInfo(fileList, this.author);
}
} }
const dataGrip = new DataGrip(); const dataGrip = new DataGrip();

View file

@ -0,0 +1,32 @@
import ICommit from 'ts/interfaces/Commit';
export function getNewFileAuthor(
addedLines: number,
prev?: ICommit | null,
) {
return {
added: addedLines,
changes: addedLines,
removed: 0,
commits: 1,
tasks: { [prev?.task || '']: 1 },
types: { [prev?.type || '']: 1 },
scopes: { [prev?.scope || '']: 1 },
};
}
export function getNewFileInfo(
name: string,
addedLines: number,
commit?: ICommit | null,
) {
return {
name,
extension: name?.split('.')?.pop(),
lines: addedLines,
created: commit,
authors: {
[commit?.author || '']: getNewFileAuthor(addedLines, commit),
},
};
}

View file

@ -1,11 +1,13 @@
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 from 'ts/interfaces/Commit';
import settingsStore from 'ts/store/Settings';
import getUserInfo from './user_info'; import getUserInfo from './user_info';
import { getNewFileName, getFileList } from './files'; import { getNewFileName, getFileList } from './files';
import settingsStore from 'ts/store/Settings'; import { getNewFileInfo } from './file_info';
const uniq = {};
export default function Parser( export default function Parser(
report: string[], report: string[],
parseCommit: Function, parseCommit: Function,
@ -62,23 +64,8 @@ export default function Parser(
delete allFiles[fileName]; delete allFiles[fileName];
} }
} else { } else {
allFiles[fileName] = { // @ts-ignore
name: fileName, allFiles[fileName] = getNewFileInfo(fileName, added, prev);
lines: added,
// @ts-ignore
created: prev,
authors: {
[prev?.author || '']: {
added: added,
changes: added,
removed: 0,
commits: 1,
tasks: { [prev?.task || '']: 1 },
types: { [prev?.type || '']: 1 },
scopes: { [prev?.scope || '']: 1 },
},
},
};
} }
if (removed > added) { if (removed > added) {
removed -= added; removed -= added;
@ -99,7 +86,15 @@ export default function Parser(
prev.removed += removed; prev.removed += removed;
} }
} else { } else {
if (prev) parseCommit(prev);
if (prev) {
if (uniq[prev.date]) {
// console.log(`double ${uniq[prev.date]} === ${i}`);
}
uniq[prev.date] = i;
parseCommit(prev);
}
const next = getUserInfo(message); const next = getUserInfo(message);
if (next.milliseconds > weekEndTime) { if (next.milliseconds > weekEndTime) {
week += 1; week += 1;
@ -115,7 +110,6 @@ export default function Parser(
} }
if (prev) parseCommit(prev); if (prev) parseCommit(prev);
const { fileList, fileTree } = getFileList(allFiles); const { fileList, fileTree } = getFileList(allFiles);
return { return {
commits, commits,

View file

@ -0,0 +1,43 @@
import ICommit from 'ts/interfaces/Commit';
import getUserInfo from './user_info';
import settingsStore from 'ts/store/Settings';
export default function ParserTelegramm(
messages: any[],
parseCommit: Function,
) {
const commits: ICommit[] = [];
let week: number = 0;
let weekEndTime: number = 0;
let prev = null;
for (let i = 0, l = messages.length; i < l; i += 1) {
const message = messages[i];
if (!message?.text) continue;
if (prev) parseCommit(prev);
const next = getUserInfo(message);
if (next.milliseconds > weekEndTime) {
week += 1;
weekEndTime = next.milliseconds + (settingsStore.ONE_DAY * (6 - next.day));
}
// @ts-ignore
next.week = week;
prev = next;
commits.push(prev);
}
if (prev) parseCommit(prev);
return {
commits,
fileList: [],
fileTree: {
id: Math.random(),
name: '',
content: {},
},
};
}

View file

@ -0,0 +1,54 @@
import ICommit from 'ts/interfaces/Commit';
export default function getUserInfo(message: any): ICommit {
/*
"id": -999973047,
"type": "message",
"date": "2021-02-03T17:21:10",
"date_unixtime": "1612362070",
"from": "Дарья Скуратова (ВТБ)",
"from_id": "user415945803",
"text": "Ребята, привет!",
"text_entities": [
{
"type": "plain",
"text": "Ребята, привет!"
}
]
*/
const date = new Date(message?.date);
const day = date.getDay() - 1;
const timestamp = message?.date.split('T')[0];
const author = message?.from || '';
const email = message?.from_id || '';
const text = Array.isArray(message?.text)
? message.text.map((subString: any) => subString?.text || subString).join(' ')
: (message?.text || '');
return {
date: message?.date,
day: day < 0 ? 6 : day,
dayInMonth: date.getDate(),
hours: date.getHours(),
minutes: date.getMinutes(),
month: date.getMonth(),
year: date.getUTCFullYear(),
week: 0,
timestamp,
milliseconds: parseInt(message?.date_unixtime, 10),
author,
email,
message: text || '',
task: 'беседа',
type: 'не подписан',
scope: 'неопределенна',
changes: 0,
added: 0,
removed: 0,
};
}

View file

@ -4,9 +4,8 @@ import ALL_ACHIEVEMENTS from './constants/list';
import byCompetition from './byCompetition'; import byCompetition from './byCompetition';
export default function getAchievementByAuthor(author: string) { export default function getAchievementByAuthor(author: string) {
const statistic = dataGrip.author.statistic.find((item: any) => item.author === author); const statistic = dataGrip.author.statisticByName[author];
if (!statistic) return; if (!statistic) return;
const list = byCompetition.get(author); const list = byCompetition.get(author);
// Сова - 70% коммитов после 15:00 // Сова - 70% коммитов после 15:00

View file

@ -1,4 +1,5 @@
import IHashMap from 'ts/interfaces/HashMap'; import IHashMap from 'ts/interfaces/HashMap';
import dataGrip from 'ts/helpers/DataGrip';
class AchievementsByCompetition { class AchievementsByCompetition {
authors: IHashMap<string[]> = {}; authors: IHashMap<string[]> = {};
@ -68,6 +69,14 @@ class AchievementsByCompetition {
// Главный редактор - сделал больше всех меток «рефакторинг» // Главный редактор - сделал больше всех меток «рефакторинг»
achievements[moreRefactoring.first].push('moreRefactoring'); achievements[moreRefactoring.first].push('moreRefactoring');
const tasksInDay = this.#getFirstAndLast(total.tasksInDay);
// Спиди-гонщик - рекорд по количеству закрытых задач в день
achievements[tasksInDay.first].push('moreTasksInDay');
const commitsInDay = this.#getFirstAndLast(total.commitsInDay);
// Zerg Rush - рекорд по количеству коммитов в день
achievements[commitsInDay.first].push('moreCommits');
this.authors = achievements; this.authors = achievements;
} }
@ -88,6 +97,10 @@ class AchievementsByCompetition {
addData('days', statistic.days); addData('days', statistic.days);
addData('moreRefactoring', statistic.types.refactor); addData('moreRefactoring', statistic.types.refactor);
const byTimestamp = dataGrip.timestamp.statisticByAuthor[statistic.author];
addData('tasksInDay', byTimestamp.tasksByTimestampCounter.max);
addData('commitsInDay', byTimestamp.commitsByTimestampCounter.max);
if (statistic.isStaff) return; if (statistic.isStaff) return;
addData('allDaysInProject', statistic.allDaysInProject); addData('allDaysInProject', statistic.allDaysInProject);
addData('lazyDays', statistic.lazyDays); addData('lazyDays', statistic.lazyDays);

View file

@ -1,15 +1,16 @@
import ACHIEVEMENT_TYPE from './type'; import ACHIEVEMENT_TYPE from './type';
export default { export default {
// готово
commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL], commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL],
commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL], commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL],
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD], commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD],
workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD], workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD],
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD], // нет картинки workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD],
userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL], userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL],
userIsDied: ['Мёртвая душа', 'работал, но уволился', ACHIEVEMENT_TYPE.NORMAL], userIsDied: ['Мёртвая душа', 'работал, но уволился', ACHIEVEMENT_TYPE.NORMAL],
lessTasks: ['Зашел и вышел', 'меньше всего закрытых задач', ACHIEVEMENT_TYPE.BAD], lessTasks: ['Зашел и вышел', 'меньше всего закрытых задач', ACHIEVEMENT_TYPE.BAD],
moreTasks: ['Батя грит малаца', 'больше всего закрытых задач', ACHIEVEMENT_TYPE.GOOD], // нет картинки moreTasks: ['Батя грит малаца', 'больше всего закрытых задач', ACHIEVEMENT_TYPE.GOOD],
everyMessageLong: ['Мастер красноречия', 'стабильно самые длинные подписи коммитов', ACHIEVEMENT_TYPE.NORMAL], everyMessageLong: ['Мастер красноречия', 'стабильно самые длинные подписи коммитов', ACHIEVEMENT_TYPE.NORMAL],
everyMessageShort: ['Болтун находка для шпиона', 'стабильно, самые короткие подписи коммитов', ACHIEVEMENT_TYPE.BAD], everyMessageShort: ['Болтун находка для шпиона', 'стабильно, самые короткие подписи коммитов', ACHIEVEMENT_TYPE.BAD],
shortestName: ['Размер не главное', 'самое короткое имя', ACHIEVEMENT_TYPE.NORMAL], // нет картинки shortestName: ['Размер не главное', 'самое короткое имя', ACHIEVEMENT_TYPE.NORMAL], // нет картинки
@ -28,19 +29,21 @@ export default {
moreDaysInProject: ['Старожил', 'больше всего дней на проекте', ACHIEVEMENT_TYPE.GOOD], moreDaysInProject: ['Старожил', 'больше всего дней на проекте', ACHIEVEMENT_TYPE.GOOD],
lessDaysInProject: ['А это кто?', 'меньше всего дней на проекте', ACHIEVEMENT_TYPE.NORMAL], lessDaysInProject: ['А это кто?', 'меньше всего дней на проекте', ACHIEVEMENT_TYPE.NORMAL],
more90DaysInProject: ['Добро пожаловать', 'не уволили на испытательном', ACHIEVEMENT_TYPE.GOOD], more90DaysInProject: ['Добро пожаловать', 'не уволили на испытательном', ACHIEVEMENT_TYPE.GOOD],
lessDaysForTask: ['Скорострел', 'работа по задачам идёт быстрее чем у остальных', ACHIEVEMENT_TYPE.GOOD], lessDaysForTask: ['Скорострел', 'одна задача занимает меньше дня', ACHIEVEMENT_TYPE.GOOD],
moreRefactoring: ['Выпускающий редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD],
// нет картинки // нет картинки
longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL], longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL],
moreRefactoring: ['Главный редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD],
adam: ['Адам', 'первый стабильны сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL], adam: ['Адам', 'первый стабильны сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL],
more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD], more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
more777DaysInProject: ['Флеш-рояль', 'отработал 777 дней на проекте', ACHIEVEMENT_TYPE.GOOD], more777DaysInProject: ['Азино 3 топора', 'отработал 777 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
moreTasksInDay: ['Спиди-гонщик', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD],
// нет кода
// moreFix: ['Bug hunter', 'больше всего закрытых багов', ACHIEVEMENT_TYPE.GOOD],
lessWorkDays: ['Дальше без меня', 'меньше всего рабочих дней', ACHIEVEMENT_TYPE.BAD], lessWorkDays: ['Дальше без меня', 'меньше всего рабочих дней', ACHIEVEMENT_TYPE.BAD],
moreTasksInDay: ['Шумахер', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD], // нет картинки
moreCreateCode: ['Созидатель', 'склонен больше остальных добавлять код', ACHIEVEMENT_TYPE.NORMAL], moreCreateCode: ['Созидатель', 'склонен больше остальных добавлять код', ACHIEVEMENT_TYPE.NORMAL],
moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL], moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL],
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], // есть картинка
moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD], moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD],
}; };

View file

@ -0,0 +1,55 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import UiKitSelect from 'ts/components/UiKit/components/Select';
import UiKitButton from 'ts/components/UiKit/components/Button';
import style from '../styles/user.module.scss';
interface IUserSelectProps {
required?: boolean;
userId: number;
onChange: Function;
}
const UserSelect = observer(({ required, userId, onChange }: IUserSelectProps): React.ReactElement => {
const authors = dataGripStore.dataGrip.author.list;
const options = authors.map((title: string, id: number) => ({ id, title }));
if (!required) {
options.unshift({ id: '', title: 'Не имеет значения' });
}
return (
<div className={style.user_select}>
<UiKitButton
type="second"
disabled={userId <= 0}
onClick={() => {
onChange(userId - 1);
}}
>
«
</UiKitButton>
<UiKitSelect
value={userId}
options={options}
className={style.user_name}
onChange={(newUserId: string) => {
onChange(newUserId);
}}
/>
<UiKitButton
type="second"
disabled={userId >= (authors.length - 1)}
onClick={() => {
onChange(userId + 1);
}}
>
»
</UiKitButton>
</div>
);
});
export default UserSelect;

View file

@ -0,0 +1,13 @@
@import '../../../../styles/variables';
.user_select {
margin-bottom: 24px;
}
.user_name {
display: inline-block;
width: 260px;
max-width: 260px;
margin: 0 12px;
}

View file

@ -16,6 +16,7 @@ import Filter from './Filter';
import { IEmployees } from '../interfaces/Setting'; import { IEmployees } from '../interfaces/Setting';
import { getNewEmployeesSettings } from '../helpers/getEmptySettings'; import { getNewEmployeesSettings } from '../helpers/getEmptySettings';
import MailMap from './MailMap';
import formStore from '../store/Form'; import formStore from '../store/Form';
import style from '../styles/index.module.scss'; import style from '../styles/index.module.scss';
@ -45,38 +46,44 @@ const SettingForm = observer((response: any): React.ReactElement | null => {
)); ));
return ( return (
<PageWrapper> <>
<PageColumn> <PageWrapper>
<Filter /> <PageColumn>
<Salary /> <Filter />
<Common /> <Salary />
</PageColumn> <Common />
<PageColumn> </PageColumn>
<Title title="Индивидуальные настройки"/> <PageColumn>
{employees.length > 0 ? ( <Title title="Индивидуальные настройки"/>
users {employees.length > 0 ? (
) : ( users
<NothingFound ) : (
message="Индивидуальных настроек нет. Данные по всем сотрудникам вычисляются по общим параметрам." <NothingFound
/> message="Индивидуальных настроек нет. Данные по всем сотрудникам вычисляются по общим параметрам."
)} />
{authors.length && ( )}
<div className={style.buttons_footer}> {authors.length && (
<UiKitButtonMenu <div className={style.buttons_footer}>
options={authors} <UiKitButtonMenu
onClick={(user: any) => { options={authors}
formStore.updateState('employees', [ onClick={(user: any) => {
...employees, formStore.updateState('employees', [
getNewEmployeesSettings(user?.title, formStore.state, selectedNames?.length), ...employees,
]); getNewEmployeesSettings(user?.title, formStore.state, selectedNames?.length),
}} ]);
> }}
Добавить пользователя >
</UiKitButtonMenu> Добавить пользователя
</div> </UiKitButtonMenu>
)} </div>
</PageColumn> )}
</PageWrapper> </PageColumn>
</PageWrapper>
<PageWrapper>
<Title title="Настройки .mailmap"/>
<MailMap />
</PageWrapper>
</>
); );
}); });

View file

@ -0,0 +1,29 @@
import React from 'react';
import dataGripStore from 'ts/store/DataGrip';
import Console from 'ts/components/Console';
import style from '../styles/index.module.scss';
function MailMap(): React.ReactElement | null {
const statistic = dataGripStore.dataGrip.author.statistic.map((item: any) => (
<p key={item.author}>
{`${item.author} <${item.firstCommit.email}> <${item.firstCommit.email}>`}
</p>
));
return (
<div className={style.races_track}>
<Console>
{statistic}
</Console>
</div>
);
}
MailMap.defaultProps = {
type: '',
canStart: false,
};
export default MailMap;

View file

@ -0,0 +1,50 @@
import IHashMap from 'ts/interfaces/HashMap';
import { IEmployees, ISetting } from '../interfaces/Setting';
import getEmptySettings from '../helpers/getEmptySettings';
class Settings {
customSettings: ISetting = getEmptySettings();
employeesByName: IHashMap<IEmployees> = {};
workDaysInMonth: number = 22;
salaryInDay: number = 180000 / 22;
update(customSettings?: ISetting) {
this.customSettings = customSettings || getEmptySettings();
this.employeesByName = this.customSettings.employees.reduce((acc, user) => {
acc[user.name] = user;
return acc;
}, {});
const salary = this.customSettings.defaultSalary;
this.workDaysInMonth = Math.ceil(4.3 * salary.workDaysInWeek);
this.salaryInDay = salary.value / this.workDaysInMonth;
}
get() {
return this.customSettings;
}
getMiddleSalaryInMonth(name: string): number {
const user = this.employeesByName[name];
if (!user) return this.customSettings.defaultSalary.value;
const salary = user.salary[user.salary.length - 1];
return salary.value;
}
getMiddleSalaryInDay(name: string) {
const user = this.employeesByName[name];
if (!user) return this.salaryInDay;
const salary = user.salary[user.salary.length - 1];
const workDaysInMonth = Math.ceil(4.3 * salary.workDaysInWeek);
return salary.value / workDaysInMonth;
}
}
const settings = new Settings();
export default settings;

View file

@ -0,0 +1,7 @@
@import '../../../../styles/variables';
.buttons_header {
display: block;
margin: 0 0 24px 0;
text-align: right;
}

View file

@ -26,7 +26,6 @@ interface ITempoViewProps {
function TempoView({ response, order, user }: ITempoViewProps) { function TempoView({ response, order, user }: ITempoViewProps) {
if (!response) return null; if (!response) return null;
console.log(response.content?.length);
return ( return (
<TempoChart <TempoChart
days={response.content as any[]} days={response.content as any[]}
@ -41,7 +40,6 @@ TempoView.defaultProps = {
}; };
function getPartOfData(filters: any, rows: any[]) { function getPartOfData(filters: any, rows: any[]) {
console.log(filters);
return rows.filter((row: any) => (row.week === filters.week)).slice(0, 7); return rows.filter((row: any) => (row.week === filters.week)).slice(0, 7);
} }
@ -53,7 +51,7 @@ const Tempo = observer((): React.ReactElement => {
const [week, setWeek] = useState<number>(firstPoint.week); const [week, setWeek] = useState<number>(firstPoint.week);
const [user, setUser] = useState<string>(''); const [user, setUser] = useState<string>('');
console.log(firstPoint.week);
if (!rows?.length) return (<NothingFound />); if (!rows?.length) return (<NothingFound />);
const partOfData = getPartOfData({ week, user }, rows); const partOfData = getPartOfData({ week, user }, rows);
const firstWeekDay = partOfData[0]; const firstWeekDay = partOfData[0];

View file

@ -0,0 +1,78 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import Title from 'ts/components/Title';
import PageWrapper from 'ts/components/Page/wrapper';
import Achievements from 'ts/components/Achievement';
import Extension from 'ts/components/Extension';
import Races from 'ts/components/Races';
import Tv100And1 from 'ts/components/Tv100And1';
import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type';
import getAchievementByAuthor from 'ts/helpers/achievement/byAuthor';
import { getDate } from 'ts/helpers/formatter';
const Top = observer((): React.ReactElement => {
const extensions = dataGripStore.dataGrip.extension.statistic
.slice(0, 4).map((statistic: any) => {
return (
<Extension
key={statistic.extension}
statistic={statistic}
/>
);
});
const tracksAuth = dataGripStore.dataGrip.author.statistic
.filter((item: any) => !item.isStaff);
const value = tracksAuth.map((statistic: any) => statistic.taskInDay);
const max = Math.max(...value);
const tracks = tracksAuth.map((statistic: any) => ({
title: statistic.author,
speed: statistic.taskInDay / max,
}));
const maxMessageLength = [...tracksAuth]
.sort((a: any, b: any) => b.maxMessageLength - a.maxMessageLength)
.map((item: any) => ({ title: item.author, value: item.maxMessageLength }));
const authors = dataGripStore.dataGrip.author.statistic.map((statistic: any) => {
const achievements = getAchievementByAuthor(statistic.author);
const from = getDate(statistic.firstCommit.date);
const to = getDate(statistic.lastCommit.date);
return (
<div key={statistic.author}>
<Title title={statistic.author}/>
{`Всего коммитов: ${statistic.commits} `}
{`Работал ${statistic.allDaysInProject} дней с ${from} по ${to} `}
<PageWrapper>
<Achievements list={[
...achievements[ACHIEVEMENT_TYPE.GOOD],
...achievements[ACHIEVEMENT_TYPE.NORMAL],
...achievements[ACHIEVEMENT_TYPE.BAD],
]} />
</PageWrapper>
</div>
);
});
return (
<>
<Title title="Скорость закрытия задач"/>
<Races tracks={tracks} />
<Title title="Максимальная длинна подписи коммита"/>
<Tv100And1 rows={maxMessageLength} />
<PageWrapper>
<div style={{ whiteSpace: 'normal' }} >
{extensions}
</div>
</PageWrapper>
{authors}
</>
);
});
export default Top;

View file

@ -63,9 +63,11 @@ class DataGripStore implements IDataGripStore {
dataGrip.firstLastCommit.minData, dataGrip.firstLastCommit.minData,
dataGrip.firstLastCommit.maxData, dataGrip.firstLastCommit.maxData,
); );
dataGrip.updateByInitialization();
dataGrip.updateByFiles(fileList);
achievements.updateByDataGrip(dataGrip.author.statistic);
} }
dataGrip.updateByInitialization();
achievements.updateByDataGrip(dataGrip.author.statistic);
this.dataGrip = null; this.dataGrip = null;
this.dataGrip = dataGrip; this.dataGrip = dataGrip;
@ -77,7 +79,7 @@ class DataGripStore implements IDataGripStore {
} }
updateChars() { // todo: remove, never use updateChars() { // todo: remove, never use
console.log('s'); console.log('need update data TODO');
return; return;
dataGrip.updateByFilters(); dataGrip.updateByFilters();
if (!dataGrip.author.list.length) return; if (!dataGrip.author.list.length) return;

View file

@ -128,7 +128,6 @@ class SettingsStore implements ISettingsStore {
halfYear: 10, halfYear: 10,
month: 2, month: 2,
}[type] || 1; }[type] || 1;
dataGripStore.updateChars(); dataGripStore.updateChars();
} }