This commit is contained in:
bakhirev 2025-06-18 15:42:38 +03:00
parent c4ee49e6f7
commit c38a04e02f
53 changed files with 1453 additions and 304 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#7F9BE0"><path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/></svg>

After

Width:  |  Height:  |  Size: 550 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#146825"><path d="M720-400v-120H600v-80h120v-120h80v120h120v80H800v120h-80Zm-360-80q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm80-80h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0-80Zm0 400Z"/></svg>

After

Width:  |  Height:  |  Size: 605 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#2C3959"><path d="M40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm720 0v-120q0-44-24.5-84.5T666-434q51 6 96 20.5t84 35.5q36 20 55 44.5t19 53.5v120H760ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm400-160q0 66-47 113t-113 47q-11 0-28-2.5t-28-5.5q27-32 41.5-71t14.5-81q0-42-14.5-81T544-792q14-5 28-6.5t28-1.5q66 0 113 47t47 113ZM120-240h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0 320Zm0-400Z"/></svg>

After

Width:  |  Height:  |  Size: 768 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ED675F"><path d="M640-520v-80h240v80H640Zm-280 40q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm80-80h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0-80Zm0 400Z"/></svg>

After

Width:  |  Height:  |  Size: 572 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#146825"><path d="M200-120q-33 0-56.5-23.5T120-200v-500q0-14 4.5-26.5T138-750l56-68q9-11 20.5-16.5T240-840h480q14 0 25.5 5.5T766-818l56 68q9 11 13.5 23.5T840-700v500q0 33-23.5 56.5T760-120H200Zm16-600h528l-34-40H250l-34 40Zm-16 520h560v-440H200v440Zm382-78 142-142-142-142-58 58 84 84-84 84 58 58Zm-202 0 58-58-84-84 84-84-58-58-142 142 142 142Zm-180 78v-440 440Z"/></svg>

After

Width:  |  Height:  |  Size: 472 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#7F9BE0"><path d="M620-163 450-333l56-56 114 114 226-226 56 56-282 282Zm220-397h-80v-200h-80v120H280v-120h-80v560h240v80H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h167q11-35 43-57.5t70-22.5q40 0 71.5 22.5T594-840h166q33 0 56.5 23.5T840-760v200ZM480-760q17 0 28.5-11.5T520-800q0-17-11.5-28.5T480-840q-17 0-28.5 11.5T440-800q0 17 11.5 28.5T480-760Z"/></svg>

After

Width:  |  Height:  |  Size: 468 B

View file

@ -0,0 +1,33 @@
import React from 'react';
import { getShortTime } from 'ts/helpers/formatter';
import style from '../index.module.scss';
interface ICommit {
date: string;
message: string;
}
interface CommitInfoProps {
commits: ICommit[];
}
export default function CommitInfo({ commits }: CommitInfoProps): React.ReactElement {
const items = commits.map((commit: any) => {
return (
<div
key={commit.date}
className={style.day_info_row}
>
<span className={style.day_info_date}>
{getShortTime(commit.date)}
</span>
<span className={style.day_info_message}>
{commit.message}
</span>
</div>
);
});
return (<>{items}</>);
}

View file

@ -0,0 +1,42 @@
import React from 'react';
import IHashMap from 'ts/interfaces/HashMap';
import { TaskLink, PRLink } from 'ts/components/ExternalLink';
import dataGrip from 'ts/helpers/DataGrip';
import CommitInfo from './CommitInfo';
import style from '../index.module.scss';
interface ICommit {
date: string;
message: string;
}
interface TaskInfoProps {
tasks: IHashMap<ICommit>;
}
export default function TaskInfo({ tasks }: TaskInfoProps): React.ReactElement {
const items = Object.entries(tasks)
.map(([task, commits]: [string, any]) => {
const taskInfo = dataGrip.tasks.statisticByName.get(task);
const milliseconds = commits[0].milliseconds;
const prId = taskInfo?.prIds?.find((id: string) => {
const pr = dataGrip.pr.pr.get(id);
return pr.dateMerge >= milliseconds;
});
return (
<div key={`${prId}${task}`}>
<div className={style.day_info_link}>
<TaskLink task={task}/>
<PRLink prId={prId}/>
</div>
<CommitInfo commits={commits}/>
</div>
);
});
return (<>{items}</>);
}

View file

@ -1,84 +1,40 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import IHashMap from 'ts/interfaces/HashMap';
import { TaskLink, PRLink } from 'ts/components/ExternalLink';
import { getShortTime } from 'ts/helpers/formatter';
import dataGrip from 'ts/helpers/DataGrip';
import TaskInfo from './components/TaskInfo';
import style from './index.module.scss';
interface ICommit {
date: string;
message: string;
interface DayEvent {
firstDay: Set<string> | undefined;
lastDay: Set<string> | undefined;
release: Set<string> | undefined;
}
type ITask = IHashMap<ICommit>;
type IDayInfo = IHashMap<ITask>;
function CommitInfo({ commits }: { commits: ICommit[] }): React.ReactElement {
const items = commits.map((commit: any) => {
return (
<div
key={commit.date}
className={style.day_info_row}
>
<span className={style.day_info_date}>
{getShortTime(commit.date)}
</span>
<span className={style.day_info_message}>
{commit.message}
</span>
</div>
);
});
return (<>{items}</>);
interface DayInfoProps {
timestamp: string;
events?: DayEvent;
}
function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
const items = Object.entries(tasks)
.map(([task, commits]: [string, any]) => {
const taskInfo = dataGrip.tasks.statisticByName.get(task);
const milliseconds = commits[0].milliseconds;
const prId = taskInfo?.prIds?.find((id: string) => {
const pr = dataGrip.pr.pr.get(id);
return pr.dateMerge >= milliseconds;
});
return (
<div key={`${prId}${task}`}>
<div className={style.day_info_link}>
<TaskLink task={task}/>
<PRLink prId={prId}/>
</div>
<CommitInfo commits={commits}/>
</div>
);
});
return (<>{items}</>);
}
interface IDayInfoProps {
day: IDayInfo;
order: string[];
events?: any;
timestamp?: string;
}
function DayInfo({ day, order, events, timestamp }: IDayInfoProps): React.ReactElement {
function DayInfo({ timestamp, events }: DayInfoProps): React.ReactElement {
const { t } = useTranslation();
const firstCommit = events?.firstCommit?.[timestamp || ''] || [];
const lastCommit = events?.lastCommit?.[timestamp || ''] || [];
let taskNumber = 0;
const items = Object.entries(day?.tasksByAuthor)
const allCommitsByTimestamp = dataGrip.timestamp.statistic.allCommitsByTimestamp;
const commitsByTimestamp = allCommitsByTimestamp.find((item: any) => item.timestamp === timestamp);
const tasksByAuthor = commitsByTimestamp.tasksByAuthor || {};
const order = dataGrip.author.list;
const items = Object.entries(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 = t('page.team.month.first');
if (lastCommit.includes(author)) suffix = t('page.team.month.last');
if (events?.firstDay?.has(author)) suffix = t('page.team.month.first');
if (events?.lastDay?.has(author)) suffix = t('page.team.month.last');
return (
<div
@ -105,7 +61,6 @@ function DayInfo({ day, order, events, timestamp }: IDayInfoProps): React.ReactE
DayInfo.defaultProps = {
events: undefined,
timestamp: undefined,
};
export default DayInfo;

View file

@ -1,43 +1,48 @@
import React from 'react';
import dataGripStore from 'ts/store/DataGrip';
import { DataGripMonth } from 'ts/helpers/DataGrip/components/month';
import { Filters } from '../interfaces/Filters';
import Day from './Day';
import IMonth from '../interfaces/Month';
import { DayEvents } from '../helpers/events';
import style from '../styles/index.module.scss';
import { getEvents } from '../helpers/day';
interface IBodyProps {
month: IMonth;
maxCommits: number;
showEvents: boolean;
max: number;
month: DataGripMonth;
events: DayEvents;
filters: Filters;
}
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function Body({
max,
month,
maxCommits,
showEvents,
events,
filters,
}: IBodyProps): React.ReactElement | null {
const firstDay = month.date.getDay() - 1;
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const lastDay = firstDay + daysInMonth[month.month];
const lastDay = firstDay + DAYS_IN_MONTH[month.month];
const allDays = (new Array(6 * 7)).fill(0);
let currentDay = 0;
const events = showEvents ? getEvents(dataGripStore) : {};
const days = allDays.map((v: any, index: number) => {
const dayInfo = month.commits[currentDay];
const dayInMonth = index - firstDay + 1;
const dayInfo = month.days[currentDay];
const eventsByDay = events.get(dayInfo?.timestamp);
if (dayInfo?.dayInMonth === (index - firstDay + 1)) {
if (dayInfo?.dayInMonth === dayInMonth) {
currentDay += 1;
return (
<Day
key={index}
month={month}
maxCommits={maxCommits}
max={max}
dayNumber={index}
dayInfo={dayInfo}
events={events}
events={eventsByDay}
filters={filters}
/>
);
}

View file

@ -1,71 +1,95 @@
import React, { useState } from 'react';
import IHashMap from 'ts/interfaces/HashMap';
import dataGripStore from 'ts/store/DataGrip';
import DayInfo from 'ts/components/DayInfo';
import Title from 'ts/components/Title';
import React from 'react';
import { getDate } from 'ts/helpers/formatter';
import { DataGripDay } from 'ts/helpers/DataGrip/components/month';
import { Filters } from '../interfaces/Filters';
import { getPercentByMax, getColor, COLORS } from '../helpers/day';
import { DayEvent } from '../helpers/events';
import dayInfoStore from '../store/DayInfo';
import IMonth from '../interfaces/Month';
import style from '../styles/index.module.scss';
import {
getPercentByMax,
getColor,
getIconUrl, getDayText,
} from '../helpers/day';
interface DayProps {
max: number;
dayNumber: number;
dayInfo: DataGripDay;
events?: DayEvent;
filters: Filters;
}
interface IDayProps {
maxCommits: number;
dayNumber: any;
month: IMonth;
dayInfo: any;
events: IHashMap<any>;
function DayIcon({ src }: { src: string }) {
return (
<img
className={style.year_chart_month_body_day_icon}
src={src}
/>
);
}
function getText(filters: Filters, events?: DayEvent) {
if (filters.firstLastDays) {
if (events?.firstDay && !events?.lastDay) {
return (<DayIcon src="./assets/chart/person_add.svg" />);
}
if (!events?.firstDay && events?.lastDay) {
return (<DayIcon src="./assets/chart/person_remove.svg" />);
}
if (events?.firstDay && events?.lastDay) {
return (<DayIcon src="./assets/chart/person_add_remove.svg" />);
}
}
if (filters.release && events?.release) {
return (<DayIcon src="./assets/chart/release.svg" />);
}
return ' ';
}
function getColorList(dayNumber: number, filters: Filters, dayInfo: DataGripDay) {
// @ts-ignore
const author = filters?.authors?.[0]?.title;
if (author && dayInfo.userCommitNumbers.has(author)) {
return COLORS.SELECTED;
}
// @ts-ignore
const type = filters?.types?.[0]?.title;
if (type && dayInfo.typeCommitNumbers.has(type)) {
return COLORS.SELECTED;
}
const weekend = [5, 6, 12, 13, 19, 20, 26, 27, 33, 34, 40, 41];
if (weekend.includes(dayNumber)) {
return COLORS.WEEKEND;
}
return COLORS.WORKS;
}
function Day({
month,
dayInfo,
maxCommits,
max,
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);
filters,
dayInfo,
}: DayProps): React.ReactElement | null {
const opacity = getPercentByMax(dayInfo.commitsNumber, max);
const colorList = getColorList(dayNumber, filters, dayInfo);
const backgroundColor = getColor(colorList, opacity);
return ( // @ts-ignore
const title = getDate(dayInfo.timestamp);
const text = getText(filters, events);
return (
<div
title={title}
id={`year_chart_day_${dayInfo?.timestamp}`}
className={style.year_chart_month_body_day}
title={`commits: ${dayInfo.commits}, tasks: ${dayInfo.tasksInDay || 0}`}
style={{
backgroundColor,
backgroundImage: iconUrl ? `url(${iconUrl})` : '',
}}
onClick={() => {
setShowInfo(!showInfo);
onClick={(event) => {
dayInfoStore.toggle(dayInfo, [event.pageX, event.pageY]);
}}
>
{showInfo ? (
<>
{'◉'}
<div className={style.year_chart_month_body_day_arrow} />
<div className={`${style.year_chart_month_body_day_info} scroll_y`}>
<Title title={getDate(dayInfo.timestamp)} />
<DayInfo // @ts-ignore
day={dayInfo}
events={events}
timestamp={dayInfo.timestamp}
order={dataGripStore.dataGrip.author.list}
/>
</div>
</>
) : text}
{text || ' '}
</div>
);
}

View file

@ -0,0 +1,67 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { observer } from 'mobx-react-lite';
import Title from 'ts/components/Title';
import { getDate } from 'ts/helpers/formatter';
import DayInfo from 'ts/components/DayInfo';
import Description from 'ts/components/Description';
import type { DayEvents, DayEvent } from '../helpers/events';
import dayInfoStore from '../store/DayInfo';
import style from '../styles/day_info.module.scss';
function getRelease(event?: DayEvent) {
if (!event?.release) return '';
const releases = Array
.from(event?.release || [])
.sort()
.join(', ');
return ` Release: ${releases}`;
}
interface DayInfoHintProps {
events: DayEvents;
}
const DayInfoHint = observer(({
events,
}: DayInfoHintProps): React.ReactElement | null => {
if (!dayInfoStore.info) return null;
const top = dayInfoStore.position?.[1] + 24;
const left = dayInfoStore.position?.[0] - 150;
const event = events.get(dayInfoStore.info.timestamp);
const release = getRelease(event);
return ReactDOM.createPortal((
<div
className={style.year_chart_day_info}
style={{
top,
left,
}}
>
<div className={`${style.year_chart_day_info_body} scroll_y`}>
<Title
title={getDate(dayInfoStore.info.timestamp)}
className={style.year_chart_day_info_title}
/>
{release ? (
<Description
text={release}
className={style.year_chart_day_info_title}
/>
) : null}
<DayInfo
timestamp={dayInfoStore.info.timestamp}
events={event}
/>
</div>
</div>
), document.body);
});
export default DayInfoHint;

View file

@ -1,19 +1,22 @@
import React from 'react';
import { getLangPrefix } from 'ts/helpers/formatter';
import { DataGripMonth } from 'ts/helpers/DataGrip/components/month';
import { getCustomDate } from 'ts/helpers/formatter';
import IMonth from '../interfaces/Month';
import style from '../styles/index.module.scss';
interface IHeaderProps {
month: IMonth;
showYear: boolean;
month: DataGripMonth;
}
function Header({
showYear,
month,
}: IHeaderProps): React.ReactElement | null {
const name = month.date.toLocaleString(getLangPrefix(), { month: 'long' });
const showYear = month.first || month.last || !month.month;
const title = showYear
? getCustomDate(month.milliseconds, { month: 'long', year: 'numeric' })
: getCustomDate(month.milliseconds, { month: 'long' });
return (
<div className={style.year_chart_month_header}>
@ -21,7 +24,7 @@ function Header({
className={style.year_chart_month_header_title}
style={{ fontWeight: showYear ? 'bold' : 100 }}
>
{name} {showYear ? month.year : ''}
{title}
</span>
</div>
);

View file

@ -1,60 +1,64 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import IHashMap from 'ts/interfaces/HashMap';
import { getShortMoney } from 'ts/helpers/formatter';
import { DataGripMonth } from 'ts/helpers/DataGrip/components/month';
import IMonth from '../interfaces/Month';
import { Filters } from '../interfaces/Filters';
import Header from './Header';
import Body from './Body';
import { DayEvents } from '../helpers/events';
import styleChart from '../styles/line.module.scss';
import style from '../styles/index.module.scss';
interface IMonthProps {
max: IHashMap<number>;
month: IMonth;
showEvents: boolean;
hideMoney?: boolean;
interface MonthProps {
max: number;
showYear: boolean;
events: DayEvents;
filters: Filters;
month: DataGripMonth;
}
function Month({
max,
showYear,
events,
filters,
month,
showEvents,
hideMoney,
}: IMonthProps): React.ReactElement | null {
let value = '';
if (month.tasks) {
value = `${month.tasks || 0}`;
}
if (!hideMoney && month.money) {
value = `${month.tasks || 0}${getShortMoney(month.money || 0, 0)}`;
}
const title = hideMoney
? 'tasks'
: 'tasks and money';
}: MonthProps): React.ReactElement | null {
const { t } = useTranslation();
return (
<div className={style.year_chart_month}>
<Header month={month}/>
<Body
<Header
month={month}
maxCommits={max.commits}
showEvents={showEvents}
showYear={showYear}
/>
<div
title={title}
className={styleChart.year_chart_month_info}
>
{value}
<Body
max={max}
month={month}
events={events}
filters={filters}
/>
<div className={styleChart.year_chart_month_info}>
<img
title={t('page.team.week.tasks')}
className={style.year_chart_month_icon}
src="./assets/chart/tasks.svg"
/>
<span title={t('page.team.week.tasks')}>
{month.tasksNumber || 0}
</span>
<img
title={t('page.team.country.chart.item')}
className={style.year_chart_month_icon}
src="./assets/chart/person.svg"
/>
<span title={t('page.team.country.chart.item')}>
{month.usersNumber || 0}
</span>
</div>
</div>
);
}
Month.defaultProps = {
hideMoney: false,
};
export default Month;

View file

@ -1,32 +1,10 @@
import IHashMap from 'ts/interfaces/HashMap';
import IMonth from '../interfaces/Month';
export function getPercentByMax(countCommit: number, max: number) {
const value = ((countCommit || 0) * 100) / max;
export function getPercentByMax(count: number, max: number) {
const value = ((count || 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
] : [
export const COLORS = {
WORKS: [
'#4162B5', // 0 1
'#617DC1', // 1 0.8
'#718AC6', // 2 0.7
@ -35,37 +13,45 @@ export function getColor(isWeekend: boolean, opacity: number): string {
'#A2B3D8', // 5 0.4
'#B2C1DE', // 6 0.3
'#C2CEE4', // 7 0.2
];
],
WEEKEND: [
'#ED675F', // 1
'#EB817C', // 0.8
'#E98E8A', // 0.7
'#E89B99', // 0.6
'#E7A8A7', // 0.5
'#E7B5B6', // 0.4
'#E6C3C4', // 0.3
'#E4CFD3', // 0.2
],
SELECTED: [
'#0E5C0C', // 1
'#2B9829', // 0.8
'#4FBF4C', // 0.7
'#6DD26A', // 0.6
'#88E185', // 0.5
'#ACE4AA', // 0.4
'#C2ECC1', // 0.3
'#E1F7E1', // 0.2
],
};
export function getColor(colors: string[], opacity: number): string {
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 '';
export function getDayWidth(wrapperWidth: number = 0) {
const minMonthWidth = 7 + 8 * 16;
const newMonthNumber = Math.floor(wrapperWidth / minMonthWidth);
const step = 0.3;
const borders = 7;
for (let px = 16; px <= 24; px += step) {
const monthWidth = borders + 8 * px;
const size = monthWidth * newMonthNumber;
if (size > wrapperWidth) return (px - step);
}
return 24;
}
function getRefAuthorByTime(list: any[], property: string) {
return list.reduce((refTimeAuthor: any, item: any) => {
if (item.isStaff) return refTimeAuthor;
if (property === 'lastCommit' && !item.isDismissed) return refTimeAuthor;
const key = item?.[property]?.timestamp;
if (!refTimeAuthor[key]) refTimeAuthor[key] = [];
refTimeAuthor[key].push(item.author);
return refTimeAuthor;
}, {});
}
export function getEvents(dataGripStore: any) {
const list = dataGripStore.dataGrip.author.statistic;
return {
firstCommit: getRefAuthorByTime(list, 'firstCommit'),
lastCommit: getRefAuthorByTime(list, 'lastCommit'),
};
}

View file

@ -0,0 +1,58 @@
import { HashMap } from 'ts/interfaces/HashMap';
export interface DayEvent {
firstDay: Set<string> | undefined;
lastDay: Set<string> | undefined;
release: Set<string> | undefined;
}
export type DayEvents = HashMap<DayEvent>;
function getDayEvent(): DayEvent {
return {
firstDay: undefined,
lastDay: undefined,
release: undefined,
};
}
function updateEvent(events: DayEvents, timestamp: string, callback: Function) {
const day = events.get(timestamp) || getDayEvent();
callback(day);
events.set(timestamp, day);
}
function getCallback(property: string, name: string) {
return function updateEventProperty(event: DayEvent) {
if (event[property]) {
event[property].add(name);
} else {
event[property] = new Set([name]);
}
};
}
function addByAuthor(events: DayEvents, authors: any[]) {
authors.forEach((user: any) => {
if (user.isStaff) return;
updateEvent(events, user.firstCommit.timestamp, getCallback('firstDay', user.author));
if (user.isDismissed) {
updateEvent(events, user.lastCommit.timestamp, getCallback('lastDay', user.author));
}
});
}
function addByRelease(events: DayEvents, releases: any[]) {
releases.forEach((release: any) => {
updateEvent(events, release.lastCommit.timestamp, getCallback('release', release.title));
});
}
export function getEvents(statisticByAuthors: any[], statisticByRelease: any[]) {
const events = new Map();
addByAuthor(events, statisticByAuthors);
addByRelease(events, statisticByRelease);
return events;
}

View file

@ -1,37 +1,28 @@
import React, { useEffect, useRef, useState } from 'react';
import MinMaxCounter from 'ts/helpers/DataGrip/components/counter';
import { DataGripMonth } from 'ts/helpers/DataGrip/components/month';
import getCommitsByMonth from './helpers/getCommitsByMonth';
import getAuthorByDate from './helpers/getAuthorByDate';
import { Filters } from './interfaces/Filters';
import DayInfoHint from './components/DayInfo';
import Month from './components/Month';
import IMonth from './interfaces/Month';
import type { DayEvents } from './helpers/events';
import { getDayWidth } from './helpers/day';
import dayInfoStore from './store/DayInfo';
import style from './styles/index.module.scss';
function getDayWidth(wrapperWidth: number, monthNumber: number) {
const step = 0.3;
const borders = 7;
for (let px = 16; px <= 24; px += step) {
const monthWidth = borders + 8 * px;
const size = monthWidth * monthNumber;
if (size > wrapperWidth) return (px - step);
}
return 24;
}
interface IYearChartProps {
maxCommits: number;
showEvents?: boolean;
wordDays: any[];
authors: any[];
max?: number;
events: DayEvents;
months: DataGripMonth[];
filters?: Filters;
}
function YearChart({
maxCommits = 100,
showEvents = true,
wordDays = [],
authors = [],
max = 100,
events,
months = [],
filters = {},
}: IYearChartProps): React.ReactElement | null {
const wrapper = useRef(null);
const [dayWidth, setDayWidth] = useState<number>(16);
@ -39,45 +30,29 @@ function YearChart({
useEffect(() => {
if (!wrapper.current) return; // @ts-ignore
const size = wrapper.current?.getBoundingClientRect() || {};
const minMonthWidth = 7 + 8 * 16;
const newMonthNumber = Math.floor(size.width / minMonthWidth);
const width = getDayWidth(size.width, newMonthNumber);
const width = getDayWidth(size?.width);
setDayWidth(width);
return () => dayInfoStore.close();
}, []);
if (!wordDays || !wordDays.length) return null;
if (!months?.length) return null;
const authorsByDate = getAuthorByDate(authors);
const months = getCommitsByMonth(wordDays, authorsByDate);
const hideMoney = authors?.length === 1;
const max = {
tasks: new MinMaxCounter(),
money: new MinMaxCounter(),
};
months.forEach((month: IMonth) => {
max.tasks.update(month.tasks);
max.money.update(month.money);
const elements = months.map((month: DataGripMonth, index: number) => {
const prev = months[index - 1];
return (
<Month
key={month.id}
max={max}
events={events}
filters={filters}
showYear={prev?.year !== month?.year}
month={month}
/>
);
});
const elements = months.map((month: IMonth) => (
<Month
key={month.id}
max={{
tasks: max.tasks.max,
money: max.money.max,
commits: maxCommits,
}}
month={month}
showEvents={showEvents}
hideMoney={hideMoney}
/>
));
const customStyle = { '--day-size': `${dayWidth.toFixed(1)}px` } as React.CSSProperties;
const daySize = dayWidth.toFixed(1);
const customStyle = { '--day-size': `${daySize}px` } as React.CSSProperties;
return (
<div
@ -86,12 +61,9 @@ function YearChart({
className={style.year_chart}
>
{elements}
<DayInfoHint events={events} />
</div>
);
}
YearChart.defaultProps = {
showEvents: true,
};
export default YearChart;

View file

@ -0,0 +1,6 @@
export interface Filters {
release?: boolean;
firstLastDays?: boolean;
types?: string[];
authors?: string[];
}

View file

@ -0,0 +1,40 @@
import { observable, action, makeObservable } from 'mobx';
import { DataGripDay } from 'ts/helpers/DataGrip/components/month';
type IPosition = [number, number];
class DayInfoStore {
info: DataGripDay | undefined = undefined;
position: IPosition = [0, 0];
constructor() {
makeObservable(this, {
info: observable,
open: action,
close: action,
});
}
toggle(dayInfo: DataGripDay, position: IPosition) {
if (this.info?.timestamp === dayInfo?.timestamp) {
this.close();
} else {
this.open(dayInfo);
this.position = position;
}
}
open(dayInfo: DataGripDay) {
this.info = dayInfo;
}
close() {
this.info = undefined;
}
}
const dayInfoStore = new DayInfoStore();
export default dayInfoStore;

View file

@ -0,0 +1,83 @@
@import 'src/styles/variables';
.year_chart_day_info {
position: absolute;
top: 0;
left: 0;
font-size: var(--font-s);
font-weight: 100;
display: inline-block;
width: 350px;
margin: 0;
padding: var(--space-l);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-m);
box-shadow: var(--space-xxs) var(--space-xxs) var(--space-xxs) var(--color-border);
background-color: var(--color-white);
color: var(--color-black);
&_body {
padding-right: var(--space-m);
max-height: 300px;
overflow-x: hidden;
}
&:after {
position: absolute;
left: 142px;
top: -8px;
content: ' ';
display: inline-block;
width: 0;
height: 0;
border: var(--space-l) solid var(--color-white);
border-right: none;
border-bottom: none;
box-shadow: -2px -2px 2px var(--color-border);
transform: rotateZ(45deg);
}
&_title {
margin: 0 auto var(--space-m);
}
//
//&_table_wrapper {
// max-height: 200px;
//}
//
//&_table {
// column-gap: var(--space-xxs)
//}
//
//&_row {
// display: flex;
// align-items: center;
// justify-content: space-between;
// padding: var(--space-xxs) 0;
//
// &:nth-child(odd) {
// background-color: var(--color-border);
// }
//}
//
//&_name {
// padding-left: var(--space-xs);
// white-space: nowrap;
// text-overflow: ellipsis;
// overflow: hidden;
//}
//
//&_value {
// display: inline-block;
// text-align: right;
// padding-right: var(--space-xs);
// width: 30%;
//}
}

View file

@ -14,6 +14,14 @@
margin: var(--day-size) calc(var(--day-size) / 2) 0;
vertical-align: top;
&_icon {
display: inline-block;
width: 16px;
height: 16px;
margin: -2px 2px 0 4px;
vertical-align: middle;
}
&_header {
display: block;
height: 24px;
@ -47,14 +55,28 @@
width: var(--day-size);
height: var(--day-size);
margin: 0 1px 1px 0;
font-size: var(--font-m);
font-weight: bold;
vertical-align: top;
line-height: var(--day-size);
overflow: hidden;
cursor: pointer;
text-align: center;
color: var(--color-black);
background-color: var(--color-border);
background-blend-mode: screen;
-webkit-print-color-adjust: exact;
&_icon {
display: inline-block;
width: 96%;
height: 96%;
margin-top: 1px;
vertical-align: top;
}
&_arrow {
position: absolute;
top: 20px;

View file

@ -0,0 +1,67 @@
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';
import { getEvents } from '../helpers/day';
interface IBodyProps {
month: IMonth;
maxCommits: number;
showEvents: boolean;
}
function Body({
month,
maxCommits,
showEvents,
}: IBodyProps): React.ReactElement | null {
const firstDay = month.date.getDay() - 1;
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 = showEvents ? getEvents(dataGripStore) : {};
const days = allDays.map((v: any, index: number) => {
const dayInfo = month.commits[currentDay];
if (dayInfo?.dayInMonth === (index - firstDay + 1)) {
currentDay += 1;
return (
<Day
key={index}
month={month}
maxCommits={maxCommits}
dayNumber={index}
dayInfo={dayInfo}
events={events}
/>
);
}
return (
<div
key={index}
className={style.year_chart_month_body_day}
style={{
opacity: (index < firstDay || index > lastDay) ? 0.3 : 1,
}}
/>
);
});
return (
<div className={style.year_chart_month_body}>
{days}
</div>
);
}
Body.defaultProps = {
rows: [],
};
export default Body;

View file

@ -0,0 +1,71 @@
import React, { useState } from 'react';
import IHashMap from 'ts/interfaces/HashMap';
import DayInfo from 'ts/components/DayInfo';
import Title from 'ts/components/Title';
import { getDate } from 'ts/helpers/formatter';
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={`commits: ${dayInfo.commits}, tasks: ${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} scroll_y`}>
<Title title={getDate(dayInfo.timestamp)} />
<DayInfo timestamp={dayInfo.timestamp} />
</div>
</>
) : text}
</div>
);
}
Day.defaultProps = {
rows: [],
};
export default Day;

View file

@ -0,0 +1,30 @@
import React from 'react';
import { getLangPrefix } from 'ts/helpers/formatter';
import IMonth from '../interfaces/Month';
import style from '../styles/index.module.scss';
interface IHeaderProps {
month: IMonth;
}
function Header({
month,
}: IHeaderProps): React.ReactElement | null {
const name = month.date.toLocaleString(getLangPrefix(), { month: 'long' });
const showYear = month.first || month.last || !month.month;
return (
<div className={style.year_chart_month_header}>
<span
className={style.year_chart_month_header_title}
style={{ fontWeight: showYear ? 'bold' : 100 }}
>
{name} {showYear ? month.year : ''}
</span>
</div>
);
}
export default Header;

View file

@ -0,0 +1,60 @@
import React from 'react';
import IHashMap from 'ts/interfaces/HashMap';
import { getShortMoney } from 'ts/helpers/formatter';
import IMonth from '../interfaces/Month';
import Header from './Header';
import Body from './Body';
import styleChart from '../styles/line.module.scss';
import style from '../styles/index.module.scss';
interface IMonthProps {
max: IHashMap<number>;
month: IMonth;
showEvents: boolean;
hideMoney?: boolean;
}
function Month({
max,
month,
showEvents,
hideMoney,
}: IMonthProps): React.ReactElement | null {
let value = '';
if (month.tasks) {
value = `${month.tasks || 0}`;
}
if (!hideMoney && month.money) {
value = `${month.tasks || 0}${getShortMoney(month.money || 0, 0)}`;
}
const title = hideMoney
? 'tasks'
: 'tasks and money';
return (
<div className={style.year_chart_month}>
<Header month={month}/>
<Body
month={month}
maxCommits={max.commits}
showEvents={showEvents}
/>
<div
title={title}
className={styleChart.year_chart_month_info}
>
{value}
</div>
</div>
);
}
Month.defaultProps = {
hideMoney: false,
};
export default Month;

View file

@ -0,0 +1,71 @@
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;
if (property === 'lastCommit' && !item.isDismissed) return refTimeAuthor;
const key = item?.[property]?.timestamp;
if (!refTimeAuthor[key]) refTimeAuthor[key] = [];
refTimeAuthor[key].push(item.author);
return refTimeAuthor;
}, {});
}
export function getEvents(dataGripStore: any) {
const list = dataGripStore.dataGrip.author.statistic;
return {
firstCommit: getRefAuthorByTime(list, 'firstCommit'),
lastCommit: getRefAuthorByTime(list, 'lastCommit'),
};
}

View file

@ -0,0 +1,97 @@
import React, { useEffect, useRef, useState } from 'react';
import MinMaxCounter from 'ts/helpers/DataGrip/components/counter';
import getCommitsByMonth from './helpers/getCommitsByMonth';
import getAuthorByDate from './helpers/getAuthorByDate';
import Month from './components/Month';
import IMonth from './interfaces/Month';
import style from './styles/index.module.scss';
function getDayWidth(wrapperWidth: number, monthNumber: number) {
const step = 0.3;
const borders = 7;
for (let px = 16; px <= 24; px += step) {
const monthWidth = borders + 8 * px;
const size = monthWidth * monthNumber;
if (size > wrapperWidth) return (px - step);
}
return 24;
}
interface IYearChartProps {
maxCommits: number;
showEvents?: boolean;
wordDays: any[];
authors: any[];
}
function YearChart({
maxCommits = 100,
showEvents = true,
wordDays = [],
authors = [],
}: IYearChartProps): React.ReactElement | null {
const wrapper = useRef(null);
const [dayWidth, setDayWidth] = useState<number>(16);
useEffect(() => {
if (!wrapper.current) return; // @ts-ignore
const size = wrapper.current?.getBoundingClientRect() || {};
const minMonthWidth = 7 + 8 * 16;
const newMonthNumber = Math.floor(size.width / minMonthWidth);
const width = getDayWidth(size.width, newMonthNumber);
console.log(width);
setDayWidth(width);
}, []);
if (!wordDays || !wordDays.length) return null;
const authorsByDate = getAuthorByDate(authors);
const months = getCommitsByMonth(wordDays, authorsByDate);
const hideMoney = authors?.length === 1;
const max = {
tasks: new MinMaxCounter(),
money: new MinMaxCounter(),
};
months.forEach((month: IMonth) => {
max.tasks.update(month.tasks);
max.money.update(month.money);
});
const elements = months.map((month: IMonth) => (
<Month
key={month.id}
max={{
tasks: max.tasks.max,
money: max.money.max,
commits: maxCommits,
}}
month={month}
showEvents={showEvents}
hideMoney={hideMoney}
/>
));
const customStyle = { '--day-size': `${dayWidth.toFixed(1)}px` } as React.CSSProperties;
return (
<div
ref={wrapper}
style={customStyle}
className={style.year_chart}
>
{elements}
</div>
);
}
YearChart.defaultProps = {
showEvents: true,
};
export default YearChart;

View file

@ -0,0 +1,97 @@
@import 'src/styles/variables';
.year_chart {
--day-size: 16px;
padding: var(--space-xs) 0;
margin: 0 calc(var(--day-size) / -2);
}
.year_chart_month {
--month-size: calc(var(--day-size) * 7 + 7px);
display: inline-block;
width: var(--month-size);
margin: var(--day-size) calc(var(--day-size) / 2) 0;
vertical-align: top;
&_header {
display: block;
height: 24px;
&_title {
font-weight: 100;
font-size: var(--font-xs);
font-family: Arial, Verdana, sans-serif;
display: block;
padding: 0;
margin: 0 auto;
text-align: center;
line-height: var(--font-m);
text-decoration: none;
vertical-align: bottom;
color: var(--color-black);
}
}
&_body {
position: relative;
display: block;
width: var(--month-size);
max-width: var(--month-size);
&_day {
position: relative;
display: inline-block;
width: var(--day-size);
height: var(--day-size);
margin: 0 1px 1px 0;
vertical-align: top;
cursor: pointer;
text-align: center;
background-color: var(--color-border);
background-blend-mode: screen;
-webkit-print-color-adjust: exact;
&_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: var(--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-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: var(--color-white);
}
}
}
}

View file

@ -0,0 +1,24 @@
@import 'src/styles/variables';
.year_chart_month {
&_info {
display: block;
font-weight: 100;
font-size: var(--font-xs);
font-family: Arial, Verdana, sans-serif;
padding: 0;
margin: var(--space-s) 0 0;
line-height: var(--font-xs);
white-space: nowrap;
text-decoration: none;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
color: var(--color-11);
}
}

View file

@ -0,0 +1,119 @@
import { HashMap } from 'ts/interfaces/HashMap';
import ICommit from 'ts/interfaces/Commit';
export interface DataGripDay {
timestamp: string;
dayInMonth: number;
commitsNumber: number;
userCommitNumbers: HashMap<number>;
typeCommitNumbers: HashMap<number>;
}
export interface DataGripMonth {
id: string;
month: number;
year: number;
date: Date;
milliseconds: number;
days: DataGripDay[];
tasksNumber: number;
usersNumber: number;
}
export default class DataGripByMonth {
months: HashMap<any> = new Map();
statistic: DataGripMonth[] = [];
maxCommitsInDay: number = 0;
clear() {
this.months.clear();
this.statistic = [];
this.maxCommitsInDay = 0;
}
#getKey(commit: ICommit) {
return `${commit.year}-${commit.month}`;
}
addCommit(commit: ICommit) {
const key = this.#getKey(commit);
const statistic = this.months.get(key);
if (statistic) {
this.#update(statistic, commit);
} else {
this.#add(commit);
}
}
#update(statistic: any, commit: ICommit) {
const days = statistic.days.get(commit.dayInMonth);
if (days) {
this.#updateDay(days, commit);
} else {
this.#addDay(statistic.days, commit);
}
statistic.tasksNumber.add(commit.task);
statistic.usersNumber.add(commit.author);
}
#add(commit: ICommit) {
const key = this.#getKey(commit);
const days = new Map();
this.#addDay(days, commit);
this.months.set(key, {
id: key,
month: commit.month,
year: commit.year,
milliseconds: commit.milliseconds,
date: new Date(commit.milliseconds),
days,
tasksNumber: new Set([commit.task]),
usersNumber: new Set([commit.author]),
});
}
#updateDay(statistic: any, commit: ICommit) {
statistic.commitsNumber += 1;
statistic.userCommitNumbers.set(
commit.author,
(statistic.userCommitNumbers.get(commit.author) || 0) + 1,
);
statistic.typeCommitNumbers.set(
commit.type,
(statistic.typeCommitNumbers.get(commit.type) || 0) + 1,
);
if (statistic.commitsNumber > this.maxCommitsInDay) {
this.maxCommitsInDay = statistic.commitsNumber;
}
}
#addDay(hashMap: any, commit: ICommit) {
hashMap.set(commit.dayInMonth, {
dayInMonth: commit.dayInMonth,
timestamp: commit.timestamp,
commitsNumber: 1,
userCommitNumbers: new Map([[ commit.author, 1]]),
typeCommitNumbers: new Map([[ commit.type, 1]]),
});
}
updateTotalInfo(dataGripByAuthor: any) {
this.statistic = Array.from(this.months.values())
.map((dot: any) => {
dot.days = Array.from(dot.days.values());
dot.tasksNumber = Array.from(dot.tasksNumber).length;
console.log(Array.from(dot.usersNumber));
dot.usersNumber = Array
.from(dot.usersNumber) // @ts-ignore
.filter((name) => !dataGripByAuthor.statisticByName[name]?.isStaff)
.length;
return dot;
})
.sort((a: DataGripMonth, b: DataGripMonth) => a.milliseconds - b.milliseconds);
this.months.clear();
}
}

View file

@ -7,6 +7,7 @@ import DataGripByTeam from './components/team';
import DataGripByScope from './components/scope';
import DataGripByType from './components/type';
import DataGripByTimestamp from './components/timestamp';
import DataGripByMonth from './components/month';
import DataGripByWeek from './components/week';
import MinMaxCounter from './components/counter';
import DataGripByGet from './components/get';
@ -35,7 +36,9 @@ class DataGrip {
type: any = new DataGripByType();
timestamp: any = new DataGripByTimestamp();
timestamp: any = new DataGripByTimestamp(); // deprecated
month: any = new DataGripByMonth();
week: any = new DataGripByWeek();
@ -66,6 +69,7 @@ class DataGrip {
this.scope.clear();
this.type.clear();
this.timestamp.clear();
this.month.clear();
this.week.clear();
this.recommendations.clear();
this.get.clear();
@ -88,6 +92,7 @@ class DataGrip {
this.scope.addCommit(commit);
this.type.addCommit(commit);
this.timestamp.addCommit(commit);
this.month.addCommit(commit);
this.get.addCommit(commit);
this.week.addCommit(commit);
this.tasks.addCommit(commit);
@ -103,6 +108,7 @@ class DataGrip {
this.scope.updateTotalInfo();
this.type.updateTotalInfo();
this.timestamp.updateTotalInfo(this.author);
this.month.updateTotalInfo(this.author);
this.week.updateTotalInfo(this.author);
this.recommendations.updateTotalInfo(this);
this.tasks.updateTotalInfo();

View file

@ -56,7 +56,7 @@ export function get2Number(time: number) {
return time < 10 ? `0${time}` : time;
}
export function getCustomDate(timestamp: string, options?: any) {
export function getCustomDate(timestamp: string | number, options?: any) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleString(getLangPrefix(), options || { day: 'numeric', month: 'long', year: 'numeric' });

View file

@ -0,0 +1,101 @@
function getRelativeWindow(event: any) {
if (!event.targetTouches
|| !event.targetTouches.length) return {
x: event.clientX,
y: event.clientY,
};
const touch = event.targetTouches[0];
return {
x: touch.clientX,
y: touch.clientY,
};
}
function getPercentRelativeElement(event: any, element: any, isWithoutLimits: any) {
const coordinates = getRelativeElement(event, element, isWithoutLimits);
const elementSize = getElementSize(element);
if (!isWithoutLimits) {
// this._addLimits(relativeMouseCoordinates, elementCoordinates);
}
return {
x: Math.round((100 / elementSize.width) * coordinates.x),
y: Math.round((100 / elementSize.height) * coordinates.y),
};
}
function addLimits(relativeMouseCoordinates: any, elementCoordinates: any) {
if (relativeMouseCoordinates.y < 0) {
relativeMouseCoordinates.y = 0;
}
if (relativeMouseCoordinates.x < 0) {
relativeMouseCoordinates.x = 0;
}
const elementHeight = elementCoordinates.bottom - elementCoordinates.top;
if (relativeMouseCoordinates.y > elementHeight) {
relativeMouseCoordinates.y = elementHeight;
}
const elementWidth = elementCoordinates.right - elementCoordinates.left;
if (relativeMouseCoordinates.x > elementWidth) {
relativeMouseCoordinates.x = elementWidth;
}
return relativeMouseCoordinates;
}
function getRelativeElement(event: any, element: any, isWithoutLimits = false, addInversion = false) {
const mouseCoordinates = getRelativeWindow(event);
const elementCoordinates = element.getBoundingClientRect();
let y = elementCoordinates.bottom - mouseCoordinates.y;
if (addInversion) y = mouseCoordinates.y - elementCoordinates.top;
const relativeMouseCoordinates = {
x: mouseCoordinates.x - elementCoordinates.left,
y: y,
};
if (!isWithoutLimits) {
addLimits(relativeMouseCoordinates, elementCoordinates);
}
return relativeMouseCoordinates;
}
function getElementSize(element: any) {
const elementCoordinates = element.getBoundingClientRect();
return {
height: elementCoordinates.bottom - elementCoordinates.top,
width: elementCoordinates.right - elementCoordinates.left,
};
}
function getRelativeDocumentForOther(event: any) {
return {
x: event.pageX,
y: event.pageY,
};
}
function getRelativeDocumentForIe(event: any) {
const html = document.documentElement;
const body = document.body;
return {
x: event.clientX + (html && html.scrollLeft || body && body.scrollLeft || 0) - (html.clientLeft || 0),
y: event.clientY + (html && html.scrollTop || body && body.scrollTop || 0) - (html.clientTop || 0),
};
}
export default function getRelativeDocument(event: any) {
if (event.pageX === null && event.clientX !== null) {
return getRelativeDocumentForIe(event);
}
return getRelativeDocumentForOther(event);
}

View file

@ -59,10 +59,7 @@ function Changes({ statistic }: IChangesProps) {
<br/>
<Title title={`${getDate(selected?.timestamp)} изменили ${selected?.addedAndChanges || '_'} строк`}/>
<PageWrapper template="box">
<DayInfo
day={selected}
order={dataGripStore.dataGrip.author.list}
/>
<DayInfo timestamp={selected?.timestamp} />
</PageWrapper>
</>
);

View file

@ -1,6 +1,5 @@
import React, { useState } from 'react';
import dataGripStore from 'ts/store/DataGrip';
import { getDate, getDateByTimestamp } from 'ts/helpers/formatter';
import Recommendations from 'ts/components/Recommendations';
@ -63,10 +62,7 @@ function Commits({ statistic }: ICommitsProps) {
selected?.commits,
)} />
<PageWrapper template="box">
<DayInfo
day={selected}
order={dataGripStore.dataGrip.author.list}
/>
<DayInfo timestamp={selected?.timestamp} />
</PageWrapper>
</>
);

View file

@ -2,7 +2,7 @@ import React from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import YearChart from 'ts/components/YearChart';
import YearChart from 'ts/components/YearChart2';
import PageWrapper from 'ts/components/Page/wrapper';
import IPersonCommonProps from '../interfaces/CommonProps';

View file

@ -21,7 +21,9 @@
@media (max-width: 900px) {
.team_country_filter_select {
display: block;
min-width: 100%;
margin-bottom: var(--space-xxl)
}
}

View file

@ -0,0 +1,72 @@
import React, { useMemo } from 'react';
import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
import UiKitCheckbox from 'ts/components/UiKit/components/Checkbox';
import SelectWithButtons from 'ts/components/UiKit/components/SelectWithButtons';
import { Filters } from 'ts/components/YearChart/interfaces/Filters';
import dataGripStore from 'ts/store/DataGrip';
import style from '../Country/styles/index.module.scss';
function getFormattedUsers(rows: any[], titleForAll: string) {
const options = rows.map((title: string, id: number) => ({ id: id + 1, title }));
options.unshift({ id: 0, title: titleForAll });
return options;
}
interface MonthFiltersProps {
filters: Filters;
onChange: Function;
}
const MonthFilters = observer(({
filters,
onChange,
}: MonthFiltersProps): React.ReactElement => {
const { t } = useTranslation();
const authors = dataGripStore.dataGrip.author.list;
const types = dataGripStore.dataGrip.type.list;
const authorsOptions = useMemo(() => getFormattedUsers(authors, t('page.team.month.authors')), [authors]);
const typesOptions = useMemo(() => getFormattedUsers(types, t('page.team.month.types')), [types]);
const update = (property: string, value: any) => {
onChange({
...filters,
[property]: value,
});
};
return (
<div className={style.team_country_filter}>
<SelectWithButtons
title="page.team.tree.filters.author"
className={style.team_country_filter_select}
value={filters?.authors?.[0] || authorsOptions[0]}
options={authorsOptions}
onChange={(id: number) => update('authors', [authorsOptions[id]])}
/>
<SelectWithButtons
title="page.team.tree.filters.author"
className={style.team_country_filter_select}
value={filters?.types?.[0] || typesOptions[0]}
options={typesOptions}
onChange={(id: number) => update('types', [typesOptions[id]])}
/>
<UiKitCheckbox
title="Релизы"
className={style.team_country_filter_checkbox}
value={filters.release}
onChange={() => update('release', !filters.release)}
/>
<UiKitCheckbox
title="Первый и последний день"
className={style.team_country_filter_checkbox}
value={filters.firstLastDays}
onChange={() => update('firstLastDays', !filters.firstLastDays)}
/>
</div>
);
});
export default MonthFilters;

View file

@ -1,22 +1,28 @@
import React from 'react';
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import dataGripStore from 'ts/store/DataGrip';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import Recommendations from 'ts/components/Recommendations';
import YearChart from 'ts/components/YearChart';
import { getEvents } from 'ts/components/YearChart/helpers/events';
import { Filters } from 'ts/components/YearChart/interfaces/Filters';
import PageWrapper from 'ts/components/Page/wrapper';
import Title from 'ts/components/Title';
import ICommonPageProps from 'ts/components/Page/interfaces/CommonPageProps';
import PageWrapper from 'ts/components/Page/wrapper';
import MonthFilters from './Filters';
const Month = observer(({
mode,
}: ICommonPageProps): React.ReactElement => {
const authors = dataGripStore.dataGrip.author.statistic;
const statistic = dataGripStore.dataGrip.timestamp.statistic;
const max = statistic.commitsByTimestampCounter.max;
const statistic = dataGripStore.dataGrip.month;
const statisticByAuthor = dataGripStore.dataGrip.author.statistic;
const statisticByRelease = dataGripStore.dataGrip.release.statistic;
const recommendations = dataGripStore.dataGrip.recommendations.team?.byTimestamp;
const events = getEvents(statisticByAuthor, statisticByRelease);
const defaultFilters = { release: false, firstLastDays: true };
const [filters, setFilters] = useState<Filters>(defaultFilters);
return (
<>
@ -27,11 +33,18 @@ const Month = observer(({
/>
)}
<Title title="page.team.month.title"/>
<PageWrapper>
<MonthFilters
filters={filters}
onChange={setFilters}
/>
</PageWrapper>
<PageWrapper template="table">
<YearChart
maxCommits={max}
authors={authors}
wordDays={statistic.allCommitsByTimestamp}
max={statistic.maxCommitsInDay}
events={events}
months={statistic.statistic}
filters={filters}
/>
</PageWrapper>
</>

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Project work calendar
§ page.team.month.first: (first work day)
§ page.team.month.last: (last work day)
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: Feature statistics
§ page.team.scope.scope: Feature
§ page.team.scope.days: Working Days

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Project work calendar
§ page.team.month.first: (first work day)
§ page.team.month.last: (last work day)
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: Feature statistics
§ page.team.scope.scope: Feature
§ page.team.scope.days: Working Days

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Calendario del proyecto
§ page.team.month.first: (first work day)
§ page.team.month.last: (last work day)
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: Estadísticas de módulos
§ page.team.scope.scope: Elaboración definitiva
§ page.team.scope.days: Siervo. día

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Calendrier du projet
§ page.team.month.first: (first work day)
§ page.team.month.last: (last work day)
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: Statistiques par module
§ page.team.scope.scope: Mise au point
§ page.team.scope.days: Esclave. jours

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Project work calendar
§ page.team.month.first: (first work day)
§ page.team.month.last: (last work day)
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: Feature statistics
§ page.team.scope.scope: Feature
§ page.team.scope.days: Working Days

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: 프로젝트
§ page.team.month.first: ( )
§ page.team.month.last: ( )
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: 기능
§ page.team.scope.scope: 특징
§ page.team.scope.days: 노예

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Project work calendar
§ page.team.month.first: (first work day)
§ page.team.month.last: (last work day)
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: Feature statistics
§ page.team.scope.scope: Feature
§ page.team.scope.days: Working Days

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Календарь работы по проекту
§ page.team.month.first: (первый рабочий день)
§ page.team.month.last: (последний рабочий день)
§ page.team.month.authors: Все сотрудники
§ page.team.month.types: Все типы
§ page.team.scope.title: Статистика по фичам
§ page.team.scope.scope: Фича
§ page.team.scope.days: Раб. дней

View file

@ -57,6 +57,8 @@ export default `
§ page.team.month.title: Календарь работы по проекту
§ page.team.month.first: (первый рабочий день)
§ page.team.month.last: (последний рабочий день)
§ page.team.month.authors: Все сотрудники
§ page.team.month.types: Все сотрудники
§ page.team.scope.title: Статистика по фичам
§ page.team.scope.scope: Фича
§ page.team.scope.days: Раб. дней

View file

@ -52,6 +52,8 @@ export default `
§ page.team.month.title: 项目工作日历
§ page.team.month.first: (first work day)
§ page.team.month.last: (last work day)
§ page.team.month.authors: All employees
§ page.team.month.types: All types
§ page.team.scope.title: 按模块划分的统计数字
§ page.team.scope.scope: 修改
§ page.team.scope.days: 工作天