JIRA-1 feat(stat): add statistic
22
README.md
|
@ -1,6 +1,6 @@
|
|||
# [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> <super_man@yahoo.com>
|
||||
```
|
||||
Подробнее про формат этого файла можно прочитать тут [https://git-scm.com/docs/gitmailmap](gitmailmap).
|
||||
Подробнее про формат этого файла можно прочитать [тут](https://git-scm.com/docs/gitmailmap).
|
||||
|
||||
### Как выгрузить данные из git?
|
||||
|
||||
|
@ -56,7 +56,7 @@ Git создаст файл `dump.git`.
|
|||
### Как посмотреть отчёт онлайн?
|
||||
|
||||
- Перейти на [сайт](https://assayo.jp/)
|
||||
- Нажать кнопку "[Демо](https://assayo.jp/demo)"
|
||||
- Нажать кнопку «[Демо](https://assayo.jp/demo)»
|
||||
- Перетащить файл `dump.git` в окно браузера
|
||||
|
||||
### Как посмотреть отчёт офлайн?
|
||||
|
@ -73,8 +73,8 @@ Git создаст файл `dump.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 - раздел сайта, страница или новый функционал одним словом)`
|
||||
- какую проблему решали `(Added avatar for user)`
|
||||
|
||||
### Как автоматизировать сбор данных (CI/CD)
|
||||
|
||||
#### Локально
|
||||
- создайте клон нужного вам репозитория;
|
||||
- скопируйте в корень папку `build`;
|
||||
- откройте `build/index.html` в браузере и добавьте в закладки;
|
||||
- добавьте ярлык на `build/assets/ci-cd.sh` в папку автозагрузки (Windows);
|
||||
|
||||
Каждый раз, при перезагрузке компьютера, скрипт будет обновлять статстику по всем данным, которые автоматически влились в основную ветку.
|
||||
|
||||
### RoadMap
|
||||
|
||||
Релизы, примерно, раз в полгода. Что дальше:
|
||||
|
||||
- больше советов и достижений;
|
||||
- итоги года / месяца, печать отчётов;
|
||||
- локализация и интернационализация;
|
||||
- разные роли для статистики (скрытие финансов);
|
||||
- разработка бекенда, интеграции с другими системами;
|
||||
- локализация и интернационализация;
|
||||
|
||||
### Пожелания, предложения, замечания
|
||||
- [alexey-bakhirev@yandex.ru](mailto:alexey-bakhirev@yandex.ru)
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.33cc195e.css",
|
||||
"main.js": "./static/js/main.405a9477.js",
|
||||
"main.css": "./static/css/main.27b68202.css",
|
||||
"main.js": "./static/js/main.fcc567df.js",
|
||||
"static/media/car.png": "./static/media/car.b8dd8738e37fe866285f.png",
|
||||
"index.html": "./index.html",
|
||||
"static/media/warning.svg": "./static/media/warning.e39a87773603f3ab157f.svg",
|
||||
"static/media/info.svg": "./static/media/info.954631f6b19e3fe9c495.svg",
|
||||
"static/media/alert.svg": "./static/media/alert.41e2b99c481139c13074.svg",
|
||||
"main.33cc195e.css.map": "./static/css/main.33cc195e.css.map",
|
||||
"main.405a9477.js.map": "./static/js/main.405a9477.js.map"
|
||||
"main.27b68202.css.map": "./static/css/main.27b68202.css.map",
|
||||
"main.fcc567df.js.map": "./static/js/main.fcc567df.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.33cc195e.css",
|
||||
"static/js/main.405a9477.js"
|
||||
"static/css/main.27b68202.css",
|
||||
"static/js/main.fcc567df.js"
|
||||
]
|
||||
}
|
|
@ -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 |
|
@ -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 |
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 725 B |
1
build/assets/achievements/moreRefactoring.svg
Normal 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 |
1
build/assets/achievements/moreTasks.svg
Normal 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 |
1
build/assets/achievements/shortestName.svg
Normal 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 |
1
build/assets/achievements/workNotWork.svg
Normal 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
|
@ -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
|
BIN
build/assets/games/car.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
|
@ -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>
|
2
build/static/css/main.27b68202.css
Normal file
1
build/static/css/main.27b68202.css.map
Normal file
3
build/static/js/main.fcc567df.js
Normal file
74
build/static/js/main.fcc567df.js.LICENSE.txt
Normal 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
|
||||
*/
|
1
build/static/js/main.fcc567df.js.map
Normal file
BIN
build/static/media/car.b8dd8738e37fe866285f.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
|
@ -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 |
|
@ -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 |
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 725 B |
1
public/assets/achievements/moreRefactoring.svg
Normal 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 |
1
public/assets/achievements/moreTasks.svg
Normal 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 |
1
public/assets/achievements/shortestName.svg
Normal 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 |
1
public/assets/achievements/workNotWork.svg
Normal 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
|
@ -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
|
BIN
public/assets/games/car.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/games/car.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/games/tv100and1.png
Normal file
After Width: | Height: | Size: 156 B |
|
@ -25,6 +25,8 @@ function getParametersFromString(text: string) {
|
|||
}
|
||||
|
||||
function renderReactApplication() {
|
||||
// @ts-ignore
|
||||
console.log(window?.report?.length);
|
||||
render(
|
||||
<React.StrictMode>
|
||||
<HashRouter>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
display: inline-block;
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 16px 0 0 90px;
|
||||
padding: 23px 0 0 90px;
|
||||
margin: 0 10px 10px 0;
|
||||
box-sizing: border-box;
|
||||
vertical-align: top;
|
||||
|
|
83
src/ts/components/Console/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
33
src/ts/components/Console/index.tsx
Normal 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;
|
|
@ -45,7 +45,8 @@
|
|||
font-size: var(--font-xs);
|
||||
display: inline-block;
|
||||
margin: 0 0 var(--space-xs) 0;
|
||||
line-height: var(--font-s);
|
||||
line-height: var(--font-m);
|
||||
vertical-align: top;
|
||||
color: var(--color-42);
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,10 @@
|
|||
margin-right: var(--space-l);
|
||||
}
|
||||
|
||||
&_message {
|
||||
width: calc(100% - 44px);
|
||||
}
|
||||
|
||||
&_row {
|
||||
display: block;
|
||||
padding: 0 0 0 32px;
|
||||
|
|
|
@ -47,22 +47,35 @@ function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
|
|||
}
|
||||
|
||||
interface IDayInfoProps {
|
||||
day: IDayInfo,
|
||||
order: string[]
|
||||
day: IDayInfo;
|
||||
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;
|
||||
console.dir(firstCommit);
|
||||
|
||||
const items = Object.entries(day?.tasksByAuthor)
|
||||
.sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0])))
|
||||
.map(([author, tasks]: [string, any]) => {
|
||||
taskNumber += Object.keys(tasks).length;
|
||||
|
||||
let suffix = '';
|
||||
if (firstCommit.includes(author)) suffix = '(первый рабочий день)';
|
||||
if (lastCommit.includes(author)) suffix = '(последний рабочий день)';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={author}
|
||||
className={style.day_info}
|
||||
>
|
||||
<h3 className={style.day_info_author}>{author}</h3>
|
||||
<h3 className={style.day_info_author}>
|
||||
{`${author} ${suffix}`}
|
||||
</h3>
|
||||
<TaskInfo tasks={tasks}/>
|
||||
</div>
|
||||
);
|
||||
|
@ -78,4 +91,9 @@ function DayInfo({ day, order }: IDayInfoProps): React.ReactElement {
|
|||
);
|
||||
}
|
||||
|
||||
DayInfo.defaultProps = {
|
||||
events: undefined,
|
||||
timestamp: undefined,
|
||||
};
|
||||
|
||||
export default DayInfo;
|
||||
|
|
19
src/ts/components/Extension/components/Icon.tsx
Normal 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;
|
28
src/ts/components/Extension/components/Line.tsx
Normal 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;
|
44
src/ts/components/Extension/index.tsx
Normal 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;
|
57
src/ts/components/Extension/styles/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
34
src/ts/components/Notifications/components/Message.tsx
Normal 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;
|
26
src/ts/components/Notifications/index.tsx
Normal 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;
|
6
src/ts/components/Notifications/interfaces/Message.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default interface IMessage {
|
||||
id?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
type?: 'error' | 'warning' | 'success' | 'info';
|
||||
}
|
48
src/ts/components/Notifications/store/index.ts
Normal 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;
|
55
src/ts/components/Notifications/styles/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
26
src/ts/components/Races/Info.tsx
Normal 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;
|
77
src/ts/components/Races/Track.tsx
Normal 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;
|
106
src/ts/components/Races/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
57
src/ts/components/Races/index.tsx
Normal 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;
|
62
src/ts/components/SplashScreen/index.module.scss
Normal 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;
|
||||
}
|
||||
}
|
56
src/ts/components/SplashScreen/index.tsx
Normal 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;
|
69
src/ts/components/SplashScreen/progress.module.scss
Normal 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;
|
||||
}
|
||||
}
|
68
src/ts/components/Table — копия/components/Body.tsx
Normal 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;
|
61
src/ts/components/Table — копия/components/Column.tsx
Normal 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;
|
64
src/ts/components/Table — копия/components/Header.tsx
Normal 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;
|
|
@ -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;
|
30
src/ts/components/Table — копия/helpers/getColumnConfigs.ts
Normal 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;
|
|
@ -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);
|
||||
}
|
41
src/ts/components/Table — копия/helpers/getDefaultProps.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
63
src/ts/components/Table — копия/index.tsx
Normal 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;
|
49
src/ts/components/Table — копия/interfaces/Column.ts
Normal 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,
|
||||
}
|
28
src/ts/components/Table — копия/styles/header.module.scss
Normal 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);
|
||||
}
|
92
src/ts/components/Table — копия/styles/index.module.scss
Normal 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);
|
||||
}
|
25
src/ts/components/Tv100And1/components/Title.tsx
Normal 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;
|
51
src/ts/components/Tv100And1/index.tsx
Normal 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;
|
90
src/ts/components/Tv100And1/styles/index.module.scss
Normal 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);
|
||||
}
|
||||
}
|
|
@ -1,47 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import Day from './Day';
|
||||
import IMonth from '../interfaces/Month';
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
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];
|
||||
}
|
||||
import { getEvents } from '../helpers/day';
|
||||
|
||||
interface IBodyProps {
|
||||
month: IMonth;
|
||||
|
@ -53,30 +17,26 @@ function Body({
|
|||
maxCommits,
|
||||
}: IBodyProps): React.ReactElement | null {
|
||||
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 lastDay = firstDay + daysInMonth[month.month];
|
||||
const allDays = (new Array(6 * 7)).fill(0);
|
||||
let currentDay = 0;
|
||||
|
||||
const events = getEvents(dataGripStore);
|
||||
const days = allDays.map((v: any, index: number) => {
|
||||
const dayInfo = month.commits[currentDay];
|
||||
|
||||
if (dayInfo?.dayInMonth === (index - firstDay + 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 (
|
||||
<div
|
||||
<Day
|
||||
key={index}
|
||||
className={style.year_chart_month_body_day}
|
||||
style={{
|
||||
backgroundColor,
|
||||
backgroundImage: iconUrl ? `url(${iconUrl})` : '',
|
||||
}}
|
||||
/>
|
||||
month={month}
|
||||
maxCommits={maxCommits}
|
||||
dayNumber={index}
|
||||
dayInfo={dayInfo}
|
||||
events={events}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
73
src/ts/components/YearChart/components/Day.tsx
Normal 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;
|
|
@ -19,9 +19,7 @@ function Month({
|
|||
}: IMonthProps): React.ReactElement | null {
|
||||
return (
|
||||
<div className={`${style.year_chart_month}`}>
|
||||
<Header
|
||||
month={month}
|
||||
/>
|
||||
<Header month={month} />
|
||||
<Body
|
||||
month={month}
|
||||
maxCommits={maxCommits}
|
||||
|
|
70
src/ts/components/YearChart/helpers/day.ts
Normal 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'),
|
||||
};
|
||||
}
|
|
@ -3,5 +3,7 @@ export default interface IWorkDay {
|
|||
year: number;
|
||||
day: number;
|
||||
dayInMonth: number;
|
||||
tasksInDay: number;
|
||||
timestamp: string;
|
||||
commits: number;
|
||||
}
|
|
@ -44,13 +44,64 @@
|
|||
max-width: var(--month-size);
|
||||
|
||||
&_day {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: var(--day-size);
|
||||
height: var(--day-size);
|
||||
height: var(--day-size);
|
||||
margin: 0 1px 1px 0;
|
||||
vertical-align: top;
|
||||
background-color: var(--color-border);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,6 +40,9 @@ export default class DataGripByAuthor {
|
|||
statistic.hours.push(commit.hours);
|
||||
statistic.messageLength.push(commit.message.length);
|
||||
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.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
|
||||
}
|
||||
|
@ -60,6 +63,7 @@ export default class DataGripByAuthor {
|
|||
commitsByDayAndHour,
|
||||
messageLength: [commit.message.length || 0],
|
||||
totalMessageLength: commit.message.length || 0,
|
||||
maxMessageLength: commit.message.length || 0,
|
||||
wordStatistics: DataGripByAuthor.#updateWordStatistics(commit),
|
||||
};
|
||||
}
|
||||
|
@ -150,6 +154,7 @@ export default class DataGripByAuthor {
|
|||
isStaff,
|
||||
|
||||
middleMessageLength,
|
||||
maxMessageLength: dot.maxMessageLength,
|
||||
commitsByDayAndHourTotal: DataGripByAuthor.getTotalCommitsByDayAndHour(dot.commitsByDayAndHour),
|
||||
wordStatistics,
|
||||
};
|
||||
|
|
88
src/ts/helpers/DataGrip/components/extension.ts
Normal 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 },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import DataGripByType from './components/type';
|
|||
import DataGripByTimestamp from './components/timestamp';
|
||||
import DataGripByWeek from './components/week';
|
||||
import MinMaxCounter from './components/counter';
|
||||
import DataGripByExtension from './components/extension';
|
||||
|
||||
class DataGrip {
|
||||
firstLastCommit: any = new MinMaxCounter();
|
||||
|
@ -27,6 +28,8 @@ class DataGrip {
|
|||
|
||||
recommendations: any = new Recommendations();
|
||||
|
||||
extension: any = new DataGripByExtension();
|
||||
|
||||
initializationInfo: any = {};
|
||||
|
||||
clear() {
|
||||
|
@ -38,6 +41,7 @@ class DataGrip {
|
|||
this.timestamp.clear();
|
||||
this.week.clear();
|
||||
this.recommendations.clear();
|
||||
this.extension.clear();
|
||||
}
|
||||
|
||||
addCommit(commit: ICommit) {
|
||||
|
@ -80,6 +84,10 @@ class DataGrip {
|
|||
});
|
||||
this.#updateTotalInfo();
|
||||
}
|
||||
|
||||
updateByFiles(fileList: any[]) {
|
||||
this.extension.updateTotalInfo(fileList, this.author);
|
||||
}
|
||||
}
|
||||
|
||||
const dataGrip = new DataGrip();
|
||||
|
|
32
src/ts/helpers/Parser/file_info.ts
Normal 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),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import { IDirtyFile } from 'ts/interfaces/FileInfo';
|
||||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
import ICommit from 'ts/interfaces/Commit';
|
||||
import settingsStore from 'ts/store/Settings';
|
||||
|
||||
import getUserInfo from './user_info';
|
||||
import { getNewFileName, getFileList } from './files';
|
||||
import settingsStore from 'ts/store/Settings';
|
||||
import { getNewFileInfo } from './file_info';
|
||||
|
||||
const uniq = {};
|
||||
export default function Parser(
|
||||
report: string[],
|
||||
parseCommit: Function,
|
||||
|
@ -62,23 +64,8 @@ export default function Parser(
|
|||
delete allFiles[fileName];
|
||||
}
|
||||
} else {
|
||||
allFiles[fileName] = {
|
||||
name: fileName,
|
||||
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 },
|
||||
},
|
||||
},
|
||||
};
|
||||
// @ts-ignore
|
||||
allFiles[fileName] = getNewFileInfo(fileName, added, prev);
|
||||
}
|
||||
if (removed > added) {
|
||||
removed -= added;
|
||||
|
@ -99,7 +86,15 @@ export default function Parser(
|
|||
prev.removed += removed;
|
||||
}
|
||||
} 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);
|
||||
if (next.milliseconds > weekEndTime) {
|
||||
week += 1;
|
||||
|
@ -115,7 +110,6 @@ export default function Parser(
|
|||
}
|
||||
if (prev) parseCommit(prev);
|
||||
|
||||
|
||||
const { fileList, fileTree } = getFileList(allFiles);
|
||||
return {
|
||||
commits,
|
||||
|
|
43
src/ts/helpers/ParserTelegramm/index.ts
Normal 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: {},
|
||||
},
|
||||
};
|
||||
}
|
54
src/ts/helpers/ParserTelegramm/user_info.ts
Normal 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,
|
||||
};
|
||||
}
|
|
@ -4,9 +4,8 @@ import ALL_ACHIEVEMENTS from './constants/list';
|
|||
import byCompetition from './byCompetition';
|
||||
|
||||
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;
|
||||
|
||||
const list = byCompetition.get(author);
|
||||
|
||||
// Сова - 70% коммитов после 15:00
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import IHashMap from 'ts/interfaces/HashMap';
|
||||
import dataGrip from 'ts/helpers/DataGrip';
|
||||
|
||||
class AchievementsByCompetition {
|
||||
authors: IHashMap<string[]> = {};
|
||||
|
@ -68,6 +69,14 @@ class AchievementsByCompetition {
|
|||
// Главный редактор - сделал больше всех меток «рефакторинг»
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -88,6 +97,10 @@ class AchievementsByCompetition {
|
|||
addData('days', statistic.days);
|
||||
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;
|
||||
addData('allDaysInProject', statistic.allDaysInProject);
|
||||
addData('lazyDays', statistic.lazyDays);
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import ACHIEVEMENT_TYPE from './type';
|
||||
|
||||
export default {
|
||||
// готово
|
||||
commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL],
|
||||
commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL],
|
||||
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD],
|
||||
workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD],
|
||||
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD], // нет картинки
|
||||
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD],
|
||||
userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL],
|
||||
userIsDied: ['Мёртвая душа', 'работал, но уволился', ACHIEVEMENT_TYPE.NORMAL],
|
||||
lessTasks: ['Зашел и вышел', 'меньше всего закрытых задач', ACHIEVEMENT_TYPE.BAD],
|
||||
moreTasks: ['Батя грит малаца', 'больше всего закрытых задач', ACHIEVEMENT_TYPE.GOOD], // нет картинки
|
||||
moreTasks: ['Батя грит малаца', 'больше всего закрытых задач', ACHIEVEMENT_TYPE.GOOD],
|
||||
everyMessageLong: ['Мастер красноречия', 'стабильно самые длинные подписи коммитов', ACHIEVEMENT_TYPE.NORMAL],
|
||||
everyMessageShort: ['Болтун находка для шпиона', 'стабильно, самые короткие подписи коммитов', ACHIEVEMENT_TYPE.BAD],
|
||||
shortestName: ['Размер не главное', 'самое короткое имя', ACHIEVEMENT_TYPE.NORMAL], // нет картинки
|
||||
|
@ -28,19 +29,21 @@ export default {
|
|||
moreDaysInProject: ['Старожил', 'больше всего дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
||||
lessDaysInProject: ['А это кто?', 'меньше всего дней на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
||||
more90DaysInProject: ['Добро пожаловать', 'не уволили на испытательном', ACHIEVEMENT_TYPE.GOOD],
|
||||
lessDaysForTask: ['Скорострел', 'работа по задачам идёт быстрее чем у остальных', ACHIEVEMENT_TYPE.GOOD],
|
||||
lessDaysForTask: ['Скорострел', 'одна задача занимает меньше дня', ACHIEVEMENT_TYPE.GOOD],
|
||||
|
||||
moreRefactoring: ['Выпускающий редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD],
|
||||
// нет картинки
|
||||
longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL],
|
||||
moreRefactoring: ['Главный редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD],
|
||||
adam: ['Адам', 'первый стабильны сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
||||
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],
|
||||
moreTasksInDay: ['Шумахер', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD], // нет картинки
|
||||
moreCreateCode: ['Созидатель', 'склонен больше остальных добавлять код', ACHIEVEMENT_TYPE.NORMAL],
|
||||
moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL],
|
||||
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL],
|
||||
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], // есть картинка
|
||||
moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD],
|
||||
};
|
||||
|
|
55
src/ts/pages/Common/components/UserSelect.tsx
Normal 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;
|
13
src/ts/pages/Common/styles/user.module.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import Filter from './Filter';
|
|||
|
||||
import { IEmployees } from '../interfaces/Setting';
|
||||
import { getNewEmployeesSettings } from '../helpers/getEmptySettings';
|
||||
import MailMap from './MailMap';
|
||||
import formStore from '../store/Form';
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
|
@ -45,38 +46,44 @@ const SettingForm = observer((response: any): React.ReactElement | null => {
|
|||
));
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PageColumn>
|
||||
<Filter />
|
||||
<Salary />
|
||||
<Common />
|
||||
</PageColumn>
|
||||
<PageColumn>
|
||||
<Title title="Индивидуальные настройки"/>
|
||||
{employees.length > 0 ? (
|
||||
users
|
||||
) : (
|
||||
<NothingFound
|
||||
message="Индивидуальных настроек нет. Данные по всем сотрудникам вычисляются по общим параметрам."
|
||||
/>
|
||||
)}
|
||||
{authors.length && (
|
||||
<div className={style.buttons_footer}>
|
||||
<UiKitButtonMenu
|
||||
options={authors}
|
||||
onClick={(user: any) => {
|
||||
formStore.updateState('employees', [
|
||||
...employees,
|
||||
getNewEmployeesSettings(user?.title, formStore.state, selectedNames?.length),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Добавить пользователя
|
||||
</UiKitButtonMenu>
|
||||
</div>
|
||||
)}
|
||||
</PageColumn>
|
||||
</PageWrapper>
|
||||
<>
|
||||
<PageWrapper>
|
||||
<PageColumn>
|
||||
<Filter />
|
||||
<Salary />
|
||||
<Common />
|
||||
</PageColumn>
|
||||
<PageColumn>
|
||||
<Title title="Индивидуальные настройки"/>
|
||||
{employees.length > 0 ? (
|
||||
users
|
||||
) : (
|
||||
<NothingFound
|
||||
message="Индивидуальных настроек нет. Данные по всем сотрудникам вычисляются по общим параметрам."
|
||||
/>
|
||||
)}
|
||||
{authors.length && (
|
||||
<div className={style.buttons_footer}>
|
||||
<UiKitButtonMenu
|
||||
options={authors}
|
||||
onClick={(user: any) => {
|
||||
formStore.updateState('employees', [
|
||||
...employees,
|
||||
getNewEmployeesSettings(user?.title, formStore.state, selectedNames?.length),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Добавить пользователя
|
||||
</UiKitButtonMenu>
|
||||
</div>
|
||||
)}
|
||||
</PageColumn>
|
||||
</PageWrapper>
|
||||
<PageWrapper>
|
||||
<Title title="Настройки .mailmap"/>
|
||||
<MailMap />
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
29
src/ts/pages/Settings/components/MailMap.tsx
Normal 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;
|
50
src/ts/pages/Settings/helpers/settings.ts
Normal 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;
|
7
src/ts/pages/Settings/styles/header.module.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
@import '../../../../styles/variables';
|
||||
|
||||
.buttons_header {
|
||||
display: block;
|
||||
margin: 0 0 24px 0;
|
||||
text-align: right;
|
||||
}
|
|
@ -26,7 +26,6 @@ interface ITempoViewProps {
|
|||
|
||||
function TempoView({ response, order, user }: ITempoViewProps) {
|
||||
if (!response) return null;
|
||||
console.log(response.content?.length);
|
||||
return (
|
||||
<TempoChart
|
||||
days={response.content as any[]}
|
||||
|
@ -41,7 +40,6 @@ TempoView.defaultProps = {
|
|||
};
|
||||
|
||||
function getPartOfData(filters: any, rows: any[]) {
|
||||
console.log(filters);
|
||||
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 [user, setUser] = useState<string>('');
|
||||
console.log(firstPoint.week);
|
||||
|
||||
if (!rows?.length) return (<NothingFound />);
|
||||
const partOfData = getPartOfData({ week, user }, rows);
|
||||
const firstWeekDay = partOfData[0];
|
||||
|
|
78
src/ts/pages/Team/components/Top.tsx
Normal 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;
|
|
@ -63,9 +63,11 @@ class DataGripStore implements IDataGripStore {
|
|||
dataGrip.firstLastCommit.minData,
|
||||
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 = dataGrip;
|
||||
|
@ -77,7 +79,7 @@ class DataGripStore implements IDataGripStore {
|
|||
}
|
||||
|
||||
updateChars() { // todo: remove, never use
|
||||
console.log('s');
|
||||
console.log('need update data TODO');
|
||||
return;
|
||||
dataGrip.updateByFilters();
|
||||
if (!dataGrip.author.list.length) return;
|
||||
|
|
|
@ -128,7 +128,6 @@ class SettingsStore implements ISettingsStore {
|
|||
halfYear: 10,
|
||||
month: 2,
|
||||
}[type] || 1;
|
||||
|
||||
dataGripStore.updateChars();
|
||||
}
|
||||
|
||||
|
|