mirror of
https://github.com/bakhirev/assayo.git
synced 2025-09-01 18:19:43 +00:00
update
This commit is contained in:
parent
c4ee49e6f7
commit
c38a04e02f
53 changed files with 1453 additions and 304 deletions
1
public/assets/chart/person.svg
Normal file
1
public/assets/chart/person.svg
Normal 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 |
1
public/assets/chart/person_add.svg
Normal file
1
public/assets/chart/person_add.svg
Normal 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 |
1
public/assets/chart/person_add_remove.svg
Normal file
1
public/assets/chart/person_add_remove.svg
Normal 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 |
1
public/assets/chart/person_remove.svg
Normal file
1
public/assets/chart/person_remove.svg
Normal 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 |
1
public/assets/chart/release.svg
Normal file
1
public/assets/chart/release.svg
Normal 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 |
1
public/assets/chart/tasks.svg
Normal file
1
public/assets/chart/tasks.svg
Normal 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 |
33
src/ts/components/DayInfo/components/CommitInfo.tsx
Normal file
33
src/ts/components/DayInfo/components/CommitInfo.tsx
Normal 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}</>);
|
||||
}
|
42
src/ts/components/DayInfo/components/TaskInfo.tsx
Normal file
42
src/ts/components/DayInfo/components/TaskInfo.tsx
Normal 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}</>);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
67
src/ts/components/YearChart/components/DayInfo.tsx
Normal file
67
src/ts/components/YearChart/components/DayInfo.tsx
Normal 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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
}
|
58
src/ts/components/YearChart/helpers/events.ts
Normal file
58
src/ts/components/YearChart/helpers/events.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
6
src/ts/components/YearChart/interfaces/Filters.ts
Normal file
6
src/ts/components/YearChart/interfaces/Filters.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface Filters {
|
||||
release?: boolean;
|
||||
firstLastDays?: boolean;
|
||||
types?: string[];
|
||||
authors?: string[];
|
||||
}
|
40
src/ts/components/YearChart/store/DayInfo.ts
Normal file
40
src/ts/components/YearChart/store/DayInfo.ts
Normal 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;
|
83
src/ts/components/YearChart/styles/day_info.module.scss
Normal file
83
src/ts/components/YearChart/styles/day_info.module.scss
Normal 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%;
|
||||
//}
|
||||
}
|
|
@ -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;
|
||||
|
|
67
src/ts/components/YearChart2/components/Body.tsx
Normal file
67
src/ts/components/YearChart2/components/Body.tsx
Normal 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;
|
71
src/ts/components/YearChart2/components/Day.tsx
Normal file
71
src/ts/components/YearChart2/components/Day.tsx
Normal 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;
|
30
src/ts/components/YearChart2/components/Header.tsx
Normal file
30
src/ts/components/YearChart2/components/Header.tsx
Normal 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;
|
60
src/ts/components/YearChart2/components/Month.tsx
Normal file
60
src/ts/components/YearChart2/components/Month.tsx
Normal 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;
|
71
src/ts/components/YearChart2/helpers/day.ts
Normal file
71
src/ts/components/YearChart2/helpers/day.ts
Normal 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'),
|
||||
};
|
||||
}
|
97
src/ts/components/YearChart2/index.tsx
Normal file
97
src/ts/components/YearChart2/index.tsx
Normal 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;
|
97
src/ts/components/YearChart2/styles/index.module.scss
Normal file
97
src/ts/components/YearChart2/styles/index.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
src/ts/components/YearChart2/styles/line.module.scss
Normal file
24
src/ts/components/YearChart2/styles/line.module.scss
Normal 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);
|
||||
}
|
||||
}
|
119
src/ts/helpers/DataGrip/components/month.ts
Normal file
119
src/ts/helpers/DataGrip/components/month.ts
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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' });
|
||||
|
|
101
src/ts/helpers/mouseCoordinates.ts
Normal file
101
src/ts/helpers/mouseCoordinates.ts
Normal 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);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
|
||||
@media (max-width: 900px) {
|
||||
.team_country_filter_select {
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
margin-bottom: var(--space-xxl)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
72
src/ts/pages/Team/components/Month/Filters.tsx
Normal file
72
src/ts/pages/Team/components/Month/Filters.tsx
Normal 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;
|
|
@ -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>
|
||||
</>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: 노예 일
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: Раб. дней
|
||||
|
|
|
@ -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: Раб. дней
|
||||
|
|
|
@ -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: 工作天
|
||||
|
|
Loading…
Add table
Reference in a new issue