diff --git a/.github/test.yml b/.github/workflows/test.yml similarity index 100% rename from .github/test.yml rename to .github/workflows/test.yml diff --git a/src/App.jsx b/src/App.jsx index a9485d1..2567bf6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,11 @@ import React, { useEffect, useState } from "react"; import Header from "./components/header"; import Index from "./pages/index"; -import PullRequests from "./pages/pullRequests"; +import PullRequestsPage from "./pages/PullRequestsPage"; import CommitsPage from "./pages/commitsPage"; -import Issues from "./pages/issues"; -import Individual from "./pages/individual"; -import Projects from "./pages/projects"; +import IssuesPage from "./pages/issuesPage"; +import IndividualPage from "./pages/individualPage"; +import ProjectsPage from "./pages/projectsPage"; import { HashRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; function App() { @@ -85,10 +85,10 @@ function App() { } /> } /> } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> ); diff --git a/src/components/dateRangeSelector.jsx b/src/components/dateRangeSelector.jsx new file mode 100644 index 0000000..60432b1 --- /dev/null +++ b/src/components/dateRangeSelector.jsx @@ -0,0 +1,22 @@ +const DateRangeSelector = ({ showHistorical, dateRange, setDateRange }) => { + if (!showHistorical) return null; + + return ( +
+ +
+ ); +}; + +export default DateRangeSelector; \ No newline at end of file diff --git a/src/components/historicalToggle.jsx b/src/components/historicalToggle.jsx new file mode 100644 index 0000000..7eabd43 --- /dev/null +++ b/src/components/historicalToggle.jsx @@ -0,0 +1,22 @@ +const HistoricalToggle = ({ showHistorical, setShowHistorical }) => { + return ( +
+
+ + +
+
+ ); +}; + +export default HistoricalToggle; \ No newline at end of file diff --git a/src/components/radarPieToggle.jsx b/src/components/radarPieToggle.jsx index e328759..8ce8e49 100644 --- a/src/components/radarPieToggle.jsx +++ b/src/components/radarPieToggle.jsx @@ -1,11 +1,12 @@ import React, { useState } from 'react'; +import usePersistentState from './usePersistentState' import RadarChart from './radarChart'; import PieChart from './pieChart'; import '../styles/radarPieToggle.css'; -const RadarPieToggle = ({ radarData, pieData, title }) => { - const [selectedChart, setSelectedChart] = useState('radar'); +const RadarPieToggle = ({ RadarPieID, radarData, pieData, title }) => { + const [selectedChart, setSelectedChart] = usePersistentState(RadarPieID,'radar'); const selectRadar = () => setSelectedChart('radar'); const selectPie = () => setSelectedChart('pie'); diff --git a/src/domain/commits.jsx b/src/domain/commits.jsx index 49bb90e..439b922 100644 --- a/src/domain/commits.jsx +++ b/src/domain/commits.jsx @@ -1,28 +1,14 @@ -export const filterHistoricData = (data, days) => { - if (days === "lifetime") return data; - - const today = new Date(); - const cutoff = new Date(today); - cutoff.setDate(today.getDate() - parseInt(days)); - const cutoffDateString = cutoff.toISOString().split("T")[0]; - - const filtered = {}; - for (const date in data) { - if (date >= cutoffDateString) { - filtered[date] = data[date]; - } - } - - return filtered; -}; +export const getGaugeDataAnonymous = (data) => { + return data.commits?.total > 0 ? ((data.commits?.total || 0) - (data.commits?.anonymous || 0)) / data.commits?.total : 0 +} export const transformCommitsDataForLineChart = (data) => { - const xDataCommits = []; + const xData = []; const userSeries = {}; for (const date in data) { - xDataCommits.push(date); - const commits = data[date].commits; + xData.push(date); + const commits = data[date].commits || {}; for (const user in commits) { if (user === 'total' || user === 'anonymous') continue; @@ -31,68 +17,136 @@ export const transformCommitsDataForLineChart = (data) => { } } - const seriesDataCommits = Object.keys(userSeries).map(user => ({ + const seriesData = Object.keys(userSeries).map(user => ({ name: user, data: userSeries[user] })); - return { xDataCommits, seriesDataCommits }; + return { xData, seriesData }; }; export const transformModifiedLinesDataForLineChart = (data) => { - const xDataModified = []; + const xData = []; const userSeries = {}; for (const date in data) { - xDataModified.push(date); - const modifiedLines = data[date].modified_lines; + xData.push(date); + const modifiedLines = data[date].modified_lines || {}; for (const user in modifiedLines) { if (user === 'total') continue; if (!userSeries[user]) userSeries[user] = []; - userSeries[user].push(modifiedLines[user].modified); + userSeries[user].push(modifiedLines[user]?.modified || 0); } } - const seriesModified = Object.keys(userSeries).map(user => ({ + const seriesData = Object.keys(userSeries).map(user => ({ name: user, data: userSeries[user] })); - return { xDataModified, seriesModified }; + return { xData, seriesData }; +}; + +export const GetRadarDataCommits = (data) => { + const radarData = {}; + for (const [user, val] of Object.entries(data.commits || {})) { + if (user !== 'total' && user !== 'anonymous') { + radarData[user] = val; + } + } + return radarData; }; -export const prepareRadarData = (modifiedLinesData) => { - const radarChartModifiedLines = {}; - for (const [user, { modified }] of Object.entries(modifiedLinesData)) { - radarChartModifiedLines[user] = modified; +export const GetRadarDataModifiedLines = (data) => { + const radarData = {}; + for (const [user, val] of Object.entries(data.modified_lines || {})) { + if (user !== 'total' && val.modified !== undefined) { + radarData[user] = val.modified; + } } - return radarChartModifiedLines; + return radarData; }; -export const getPieChartData = (dataObj, key = null) => { - return Object.entries(dataObj) +export const getPieChartDataCommits = (data) => { + return Object.entries(data.commits || {}) .filter(([user]) => user !== 'total' && user !== 'anonymous') - .map(([user, val]) => [user, key ? val[key] : val]); + .map(([user, val]) => [user, val]); }; -export const getGaugeChartPercentages = (dataObj, totalKey, anonymousKey = null) => { - const total = dataObj[totalKey]; - const anonymous = anonymousKey ? dataObj[anonymousKey] : 0; - return Object.entries(dataObj) - .filter(([user]) => user !== totalKey && user !== anonymousKey) +export const getPieChartDataModifiedLines = (data) => { + return Object.entries(data.modified_lines || {}) + .filter(([user]) => user !== 'total') + .map(([user, val]) => [user, val.modified]); +}; + +export const getGaugeChartDataCommits = (data) => { + const total = data.commits?.total || 0; + const anonymous = data.commits?.anonymous || 0; + const denominator = total - anonymous; + + return Object.entries(data.commits || {}) + .filter(([user]) => user !== 'total' && user !== 'anonymous') .map(([user, val]) => ({ user, - percentage: (total - anonymous) > 0 ? val / (total - anonymous) : 0 + percentage: denominator > 0 ? val / denominator : 0, })); }; -export const getGaugeChartModifiedLines = (dataObj, totalKey) => { - const total = dataObj[totalKey].modified; - return Object.entries(dataObj) - .filter(([user]) => user !== totalKey) +export const getGaugeChartDataModifiedLines = (data) => { + const totalModified = data.modified_lines?.total?.modified || 0; + + return Object.entries(data.modified_lines || {}) + .filter(([user]) => user !== 'total') .map(([user, val]) => ({ user, - percentage: total > 0 ? val.modified / total : 0 + percentage: totalModified > 0 ? val.modified / totalModified : 0, })); }; + +export const transformCommitsDataForUser = (data, username) => { + const xDataCommits = []; + const yDataCommits = []; + + for (const date in data) { + xDataCommits.push(date); + yDataCommits.push(data[date].commits[username] || 0); + } + + return { xDataCommits, yDataCommits }; + }; + +export const transformModifiedLinesDataForUser = (data, username) => { + const xDataModifiedLines = []; + const yDataModifiedLines = []; + + for (const date in data) { + const userData = data[date].modified_lines[username].modified; + xDataModifiedLines.push(date); + yDataModifiedLines.push(userData); + } + + return { xDataModifiedLines, yDataModifiedLines }; + }; + + export const transformDataForLineChartCommits = (data) => { + const xData = []; + const yData = []; + for (const date in data) { + xData.push(date); + yData.push(data[date].commits.total); + } + + return { xData, yData }; + }; + + export const transformDataForLineChartModifiedLines = (data) => { + const xDataModifedLines = []; + const yDataModifedLines = []; + for (const date in data) { + xDataModifedLines.push(date); + yDataModifedLines.push(data[date].modified_lines.total.modified); + } + + return { xDataModifedLines, yDataModifedLines }; + }; diff --git a/src/domain/issues.jsx b/src/domain/issues.jsx new file mode 100644 index 0000000..dfa10eb --- /dev/null +++ b/src/domain/issues.jsx @@ -0,0 +1,123 @@ + +export const transformAssignedIssuesDataForLineChart = (data) => { + const xDataAssigned = []; + const userSeries = {}; + + for (const date in data) { + xDataAssigned.push(date); + const issues = data[date].issues.assigned; + + for (const user in issues) { + if (user === 'non_assigned') continue; + if (!userSeries[user]) { + userSeries[user] = []; + } + userSeries[user].push(issues[user]); + } + } + + const seriesDataAssigned = Object.keys(userSeries).map(user => ({ + name: user, + data: userSeries[user] + })); + + return { xDataAssigned, seriesDataAssigned }; +}; + +export const getRadarAndPieDataIssuesAssigned = (data) => { + const radarData = {}; + const assigned = data.issues.assigned || {}; + for (const [user, val] of Object.entries(assigned)) { + if (user !== 'non_assigned' && user !== 'total') { + radarData[user] = val; + } + } + const pieData = Object.entries(radarData) + return { radarData, pieData }; +}; + +export const getGaugeDataIssuesAssigned = (data) => { + const total = data.issues.total || 0; + const nonAssigned = data.issues.assigned['non_assigned'] || 0; + const assigned = total - nonAssigned; + return total > 0 ? assigned / total : 0; +}; + +export const getGaugeDataAssignedIssuesPerUser = (data) => { + const total = data.issues.total || 0; + const nonAssigned = data.issues.assigned['non_assigned'] || 0; + const totalAssigned = total - nonAssigned; + + return Object.entries(data.issues.assigned) + .filter(([user]) => user !== 'non_assigned') + .map(([user, assigned]) => ({ + user, + percentage: totalAssigned > 0 ? assigned / totalAssigned : 0, + })); +}; + +export const getGaugeDataClosedIssuesPerUser = (data) => { + return Object.entries(data.issues.closed).map(([user, closed]) => { + const assigned = data.issues.assigned[user] || 0; + return { + user, + percentage: assigned > 0 ? closed / assigned : 0, + }; + }); +}; + +export const getGaugeDataIssuesHavePR = (data) => { + const totalClosed = data.issues.total_closed || 0; + const havePullRequest = data.issues.have_pull_request || 0; + return totalClosed > 0 ? havePullRequest / totalClosed : 0; +}; + +export const transformAssignedIssuesDataForUser = (data, username) => { + const xDataAssignedIssues = []; + const yDataAssignedIssues = []; + + for (const date in data) { + xDataAssignedIssues.push(date); + yDataAssignedIssues.push(data[date].issues?.assigned?.[username] || 0); + } + + return { xDataAssignedIssues, yDataAssignedIssues }; + }; + +export const transformClosedIssuesDataForUser = (data, username) => { + const xDataClosedIssues = []; + const yDataClosedIssues = []; + + for (const date in data) { + xDataClosedIssues.push(date); + yDataClosedIssues.push(data[date].issues?.closed?.[username] || 0); + } + + return { xDataClosedIssues, yDataClosedIssues }; + }; + +export const transformIssuesDataForAreaChart = (data) => { + const xDataIssues = []; + const closedIssues = []; + const openIssues = []; + + for (const date in data) { + const total = data[date].issues.total || 0; + const closed = data[date].issues.total_closed || 0; + const open = total - closed; + xDataIssues.push(date); + closedIssues.push(closed); + openIssues.push(open); + } + return { xDataIssues, closedIssues, openIssues }; + }; + +export const getPieDataIssuesStatus = (data) => { + const issues = data.issues + const pieDataIssuesStatus = [ + ["Open",issues.total - issues.total_closed], + ["Closed",issues.total_closed] + ] + const pieDataIssuesStatusColor = ["red","green"]; + return {pieDataIssuesStatus,pieDataIssuesStatusColor}; +} \ No newline at end of file diff --git a/src/domain/projects.jsx b/src/domain/projects.jsx new file mode 100644 index 0000000..db0d266 --- /dev/null +++ b/src/domain/projects.jsx @@ -0,0 +1,504 @@ +export const getActiveIteration = (data) => { + const has_iterations = data.project.has_iterations; + const today = new Date(); + if (!has_iterations) return "total"; + const iterations = data.project.iterations + for (const key in iterations) { + const { startDate, endDate } = iterations[key]; + const start = new Date(startDate); + const end = new Date(endDate); + + if (today >= start && today <= end) { + return key; + } + } + return "total"; + }; + +export const filterHistoricDataByIteration = (data,dataHist, iterationName) => { + if (!data) return null; + if (iterationName === "total") return dataHist; + + const iteration = data.project.iterations[iterationName] || null; + if (!iteration) return dataHist; + + const startDate = new Date(iteration.startDate); + const endDate = new Date(iteration.endDate); + + const filtered = {}; + for (const dateStr in dataHist) { + const date = new Date(dateStr); + if (date >= startDate && date <= endDate) { + filtered[dateStr] = dataHist[dateStr]; + } + } + return filtered; + }; + +export const transformAssignedTasksDataForLineChart = (data,dataHistoric, selectedIteration, iterations) => { + let allDates = []; + if (!dataHistoric) return { xDataAssigned: [], seriesDataAssigned: [] } + + if (selectedIteration === "total") { + allDates = Object.keys(dataHistoric).sort((a,b) => new Date(a) - new Date(b)); + } else { + const iteration = iterations[selectedIteration]; + if (!iteration) return { xDataAssigned: [], seriesDataAssigned: [] }; + + const startDate = new Date(iteration.startDate); + const endDate = new Date(iteration.endDate); + + const dates = []; + let currentDate = new Date(startDate); + while (currentDate <= endDate) { + const dateStr = currentDate.toISOString().split('T')[0]; + dates.push(dateStr); + currentDate.setDate(currentDate.getDate() + 1); + } + allDates = dates; + } + + const userSeries = {}; + const allUsers = new Set(); + + const assignedPerMember = data.project.metrics_by_iteration["total"]?.assigned_per_member || {}; + + Object.keys(assignedPerMember) + .filter(u => u !== "non_assigned") + .forEach(u => allUsers.add(u)); + + allUsers.forEach(u => { + userSeries[u] = []; + }); + + for (const date of allDates) { + const dayData = dataHistoric[date]; + const iterationData = dayData?.project?.metrics_by_iteration?.[selectedIteration]; + const assignedPerMember = iterationData?.assigned_per_member || {}; + allUsers.forEach(u => { + userSeries[u].push(assignedPerMember[u] || null); + }); + } + + const seriesDataAssigned = Array.from(allUsers).map(u => ({ + name: u, + data: userSeries[u], + })); + + return { xDataAssigned: allDates, seriesDataAssigned }; +}; + +export const transformFeatureDataForAreaChart = (dataHistoric, selectedIteration, iterations) => { + if (!dataHistoric) return { xDataFeature: [], allSeries: [] }; + + let allDates = []; + + if (selectedIteration === "total") { + allDates = Object.keys(dataHistoric).sort((a, b) => new Date(a) - new Date(b)); + } else { + const iteration = iterations[selectedIteration]; + if (!iteration) return { xDataFeature: [], allSeries: [] }; + + const startDate = new Date(iteration.startDate); + const endDate = new Date(iteration.endDate); + + let currentDate = new Date(startDate); + while (currentDate <= endDate) { + const dateStr = currentDate.toISOString().split('T')[0]; + allDates.push(dateStr); + currentDate.setDate(currentDate.getDate() + 1); + } + } + + const knownKeys = { + total_features_done: 'Done', + total_features_in_progress: 'In Progress', + total_features_todo: 'To Do', + }; + + const baseFeatureData = { + 'Done': [], + 'In Progress': [], + 'To Do': [], + }; + + const otherKeysData = {}; + const allOtherKeys = new Set(); + + // Identificar claves adicionales desde los datos totales (ya que puede no haber por iteración) + for (const date of allDates) { + const metrics = dataHistoric[date]?.project?.metrics_by_iteration?.total; + if (!metrics) continue; + + for (const [key, value] of Object.entries(metrics)) { + if ( + !Object.keys(knownKeys).includes(key) && + typeof value === 'number' && + key.startsWith('total_features_') && + Number.isInteger(value) + ) { + allOtherKeys.add(key); + } + } + } + + for (const key of allOtherKeys) { + otherKeysData[key] = []; + } + + const xDataFeature = []; + + for (const date of allDates) { + const metrics = + selectedIteration === "total" + ? dataHistoric[date]?.project?.metrics_by_iteration?.total + : dataHistoric[date]?.project?.metrics_by_iteration?.[selectedIteration] + + xDataFeature.push(date); + for (const [key, label] of Object.entries(knownKeys)) { + baseFeatureData[label].push(metrics?.[key] || 0); + } + + for (const key of allOtherKeys) { + const value = metrics?.[key]; + otherKeysData[key].push(Number.isInteger(value) ? value : 0); + } + } + + const baseSeries = [ + { label: 'Done', data: baseFeatureData['Done'], color: 'rgb(0, 255, 0)' }, + { label: 'In Progress', data: baseFeatureData['In Progress'], color: 'orange' }, + { label: 'To Do', data: baseFeatureData['To Do'], color: 'rgb(255, 0, 0)' } + ]; + + const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({ + label: key + .replace('total_features_', '') + .replace(/_/g, ' ') + .replace(/\b\w/g, c => c.toUpperCase()), + data + })); + + const allSeries = [...baseSeries, ...otherSeries]; + return { xDataFeature, allSeries }; +}; + +const formatDate = (dateStr) => { + const d = new Date(dateStr); + const day = String(d.getDate()).padStart(2, '0'); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const year = d.getFullYear(); + return `${day}/${month}/${year}`; +} + +export const getRadarPieDataAssignedTasks = (data,iteration) => { + const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member; + const radarDataAssigned = assignedPerMember; + const pieDataAssigned = Object.entries(radarDataAssigned) + return {radarDataAssigned,pieDataAssigned} +} +export const getDateRangeForIteration = (data,iterationName) => { + const iteration = data.project.iterations[iterationName]; + + if (!iteration) return ''; + return `${formatDate(iteration.startDate)} - ${formatDate(iteration.endDate)}`; + } + +export const getIssueTypeDataForChart = (projectData,selectedIteration) => { + + const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration]; + + if (!iterationData) return []; + + const { + total_tasks = 0, + total_bugs = 0, + total_features = 0, + } = iterationData; + const typeColorsPieChart = ["orange", "#0f58ff","red"]; + const typePieChartData = [ + ['Tasks', total_tasks], + ['Features', total_features], + ['Bugs', total_bugs], + ] + return { typePieChartData, typeColorsPieChart} + }; + +export const getFeatureDataForChart = (projectData, selectedIteration) => { + const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration]; + if (!iterationData) return { baseData: [], otherData: [] }; + + const baseData = []; + const otherData = []; + + const knownKeys = { + total_features_todo: 'To Do', + total_features_in_progress: 'In Progress', + total_features_done: 'Done', + }; + + for (const [key, label] of Object.entries(knownKeys)) { + const value = iterationData[key] || 0; + baseData.push({ label, value }); + } + + for (const [key, value] of Object.entries(iterationData)) { + if ( + key.startsWith('total_features_') && + !knownKeys.hasOwnProperty(key) + ) { + const label = key + .replace('total_features_', '') + .replace(/_/g, ' ') + .replace(/\b\w/g, c => c.toUpperCase()); + otherData.push({ label, value }); + } + } + const featurePieChartData = [ + ...baseData.map(item => [item.label, item.value]), + ...otherData.map(item => [item.label, item.value]) + ]; const featureColorsPieChart = ["green", "orange","red"]; + return {featurePieChartData,featureColorsPieChart}; +} + +export const getGaugeDataTasksAssigned = (data,iteration) => { + const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member; + const totalTasks = data.project.metrics_by_iteration[iteration].total_tasks; + const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0); + const percentageTasksAssigned = totalTasks > 0 ? totalAssigned / totalTasks : 0; + return percentageTasksAssigned; +} + +export const getGaugeDataTasksStandardStatus = (data,iteration) => { + const totalTasks = data.project.metrics_by_iteration[iteration].total_tasks; + const totalInProgress = data.project.metrics_by_iteration[iteration].in_progress || 0 + const totalDone = data.project.metrics_by_iteration[iteration].done || 0 + const totalToDo = data.project.metrics_by_iteration[iteration].todo || 0 + + const totalStandardStatuses = totalInProgress + totalDone + totalToDo + const percentageStandardStatus = totalTasks > 0 ? totalStandardStatuses / totalTasks : 0; + return percentageStandardStatus; +} + +export const getGaugeDataItemsIssues = (data,iteration) => { + const totalIssues = data.project.metrics_by_iteration[iteration].total_issues; + const totalItems = data.project.metrics_by_iteration[iteration].total + const percentageItemsIssues = totalItems > 0 ? totalIssues / totalItems : 0; + return percentageItemsIssues; +} + +export const getGaugeDataItemIssuesWithType= (data,iteration) => { + const totalIssues = data.project.metrics_by_iteration[iteration].total_issues; + const totalIssuesWithType = data.project.metrics_by_iteration[iteration].total_issues_with_type + const percentageItemIssuesWithType = totalIssues > 0 ? totalIssuesWithType / totalIssues : 0 + + return percentageItemIssuesWithType; +} + +export const getGaugeDataItemIssuesWithIteration= (data) => { + const taskDataNoIteration = data.project.metrics_by_iteration['no_iteration'] + const taskDataTotal = data.project.metrics_by_iteration['total'] + const itemsTotal = taskDataTotal.total + const itemsNoIteration = taskDataNoIteration.total + const percentageIteration = itemsTotal > 0 ? (itemsTotal - itemsNoIteration) / itemsTotal : 0 + + return percentageIteration; +} + +export const getGaugeDataAssignedTasksPerUser = (data,iteration) => { + const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member; + const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0); + return Object.entries(assignedPerMember).map(([user, userTasks]) => { + return { + user, + percentage: totalAssigned > 0 ? userTasks / totalAssigned : 0, + }; + }); +}; + +export const getGaugeDataInProgressTasksPerUser = (data,iteration) => { + const inProgresPerMember = data.project.metrics_by_iteration[iteration].in_progress_per_member || 0; + return Object.entries(inProgresPerMember).map(([user, inProgress]) => { + return { + user, + percentage: inProgress > 0 ? 1 : 0, + }; + }); +}; + +export const getGaugeDataDoneTasksPerUser = (data,iteration) => { + const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member; + const donePerMember = data.project.metrics_by_iteration[iteration].done_per_member || 0; + return Object.entries(donePerMember).map(([user, done]) => { + const assigned = assignedPerMember[user] + return { + user, + percentage: assigned > 0 ? done / assigned : 0, + }; + }); +}; + +export const getGaugeDataStandardStatusTasksPerUser = (data,iteration) => { + const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration[iteration].assigned_per_member; + const donePerMember = data.project.metrics_by_iteration[iteration].done_per_member || 0; + const inProgressPerMember = data.project.metrics_by_iteration[iteration].in_progress_per_member || 0; + const toDoPerMember = data.project.metrics_by_iteration[iteration].todo_per_member || 0; + return Object.entries(assignedPerMember).map(([user, assigned]) => { + const done = donePerMember[user] + const inProgress = inProgressPerMember[user] + const toDo = toDoPerMember[user] + const standardStatus = done + inProgress + toDo + return { + user, + percentage: assigned > 0 ? standardStatus / assigned : 0, + }; + }); +}; + +export const transformTasksAssignedDataForUser = (data, username) => { + const xDataTasksAssigned = []; + const yDataTasksAssigned = []; + + for (const date in data) { + xDataTasksAssigned.push(date); + const assigned = data[date]?.project?.metrics_by_iteration?.total?.assigned_per_member?.[username] || 0; + yDataTasksAssigned.push(assigned); + } + + return { xDataTasksAssigned, yDataTasksAssigned }; + }; + +export const transformTasksToDoDataForUser = (data, username) => { + const xDataTasksToDo = []; + const yDataTasksToDo = []; + + for (const date in data) { + xDataTasksToDo.push(date); + yDataTasksToDo.push(data[date].project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0); + } + + return { xDataTasksToDo, yDataTasksToDo }; + }; + +export const transformTasksInProgressDataForUser = (data, username) => { + const xDataTasksInProgress = []; + const yDataTasksInProgress = []; + + for (const date in data) { + xDataTasksInProgress.push(date); + yDataTasksInProgress.push(data[date].project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0); + } + + return { xDataTasksInProgress, yDataTasksInProgress }; + }; + + +export const transformTasksDoneDataForUser = (data, username) => { + const xDataTasksDone = []; + const yDataTasksDone = []; + + for (const date in data) { + xDataTasksDone.push(date); + const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0; + yDataTasksDone.push(done); + } + + return { xDataTasksDone, yDataTasksDone }; + }; + +export const transformTasksStandardDataForUser = (data, username) => { + const xDataTasksStandard = []; + const yDataTasksStandard = []; + + for (const date in data) { + xDataTasksStandard.push(date); + const todo = data[date]?.project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0; + const inProgress = data[date]?.project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0; + const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0; + yDataTasksStandard.push(todo + inProgress + done); + } + + return { xDataTasksStandard, yDataTasksStandard }; + }; + +export const transformTaskDataForAreaChart = (data) => { + const xDataTask = []; + const doneTask = []; + const inProgressTask = []; + const toDoTask = [] + const otherKeysData = {} + const allOtherKeys = new Set(); + + for (const date in data) { + const metrics = data[date].project.metrics_by_iteration.total; + for (const [key, value] of Object.entries(metrics)) { + if ( + !['done', 'in_progress', 'todo', 'total'].includes(key) && + !key.startsWith('total_') && + !key.startsWith('total') && + typeof value === 'number' && + Number.isInteger(value) + ) { + allOtherKeys.add(key); + } + } + } + + for (const key of allOtherKeys) { + otherKeysData[key] = []; + } + for (const date in data) { + const metrics = data[date].project.metrics_by_iteration.total; + const done = metrics.done || 0; + const todo = metrics.todo || 0; + const inProgress = metrics.in_progress || 0; + + xDataTask.push(date); + doneTask.push(done); + inProgressTask.push(inProgress); + toDoTask.push(todo) + for (const key of allOtherKeys) { + const value = metrics[key]; + otherKeysData[key].push(Number.isInteger(value) ? value : 0); + } + } + const baseSeries = [ + { label: 'Done', data: doneTask, color: "rgb(0, 255, 0)" }, + { label: 'In Progress', data: inProgressTask, color: 'orange' }, + { label: 'To Do', data: toDoTask, color: "rgb(255, 0, 0)" } + ]; + + const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({ + label: key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), + data, + })); + const allSeries = [...baseSeries, ...otherSeries]; + + return { xDataTask, allSeries }; + }; + +export const getPieDataTasksStatus = (data) => { + const taskData = data.project.metrics_by_iteration.total; + const totalInProgress = taskData.in_progress; + const totalDone = taskData.done; + const totalToDo = taskData.todo; + const pieDataTasksStatus = [ + ["Todo",totalToDo], + ["In Progress",totalInProgress], + ["Done", totalDone] + ] + for (const [key, value] of Object.entries(taskData)) { + if ( + !["todo", "in_progress", "done", "total"].includes(key) && + !key.startsWith("total_") && + !key.startsWith("total") && + typeof value === "number" && + Number.isInteger(value) + ) { + const label = key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); + pieDataTasksStatus.push([label, value]); + } + } + const pieDataTasksStatusColor = ["red", "orange","green"]; + return {pieDataTasksStatus,pieDataTasksStatusColor}; +} \ No newline at end of file diff --git a/src/domain/pullRequests.jsx b/src/domain/pullRequests.jsx new file mode 100644 index 0000000..17fb0e5 --- /dev/null +++ b/src/domain/pullRequests.jsx @@ -0,0 +1,170 @@ +export const transformCreatedPRsDataForLineChart = (data) => { + const xDataCreated = []; + const userSeries = {}; + + for (const date in data) { + xDataCreated.push(date); + const created = data[date].pull_requests.created; + + for (const user in created) { + if (!userSeries[user]) { + userSeries[user] = []; + } + userSeries[user].push(created[user]); + } + } + const seriesDataCreated = Object.keys(userSeries).map(user => ({ + name: user, + data: userSeries[user] + })); + + return { xDataCreated, seriesDataCreated }; +}; + +export const transformMergedPRsDataForLineChart = (data) => { + const xDataMerged = []; + const userSeries = {}; + + for (const date in data) { + xDataMerged.push(date); + const merged = data[date].pull_requests.merged_per_member; + + for (const user in merged) { + if (!userSeries[user]) { + userSeries[user] = []; + } + userSeries[user].push(merged[user]); + } + } + const seriesDataMerged = Object.keys(userSeries).map(user => ({ + name: user, + data: userSeries[user] + })); + + return { xDataMerged, seriesDataMerged }; +}; + +export const getRadarAndPieDataForCreatedPRs = (data) => { + const radarDataCreated = data.pull_requests.created + const pieDataCreated = Object.entries(radarDataCreated) + return {radarDataCreated,pieDataCreated} +} + +export const getRadarAndPieDataForMergedPRs = (data) => { + const radarDataMerged = data.pull_requests.merged_per_member + + const pieDataMerged = Object.entries(radarDataMerged) + return {radarDataMerged,pieDataMerged} +} + +export const getGaugeChartDataMergedPRs = (data) => { + const total = data.pull_requests.total + const totalMerged = data.pull_requests.merged + const totalClosed = data.pull_requests.closed + + const percentageMerged = (total - totalClosed) > 0 ? totalMerged / (total - totalClosed) : 0 + + return percentageMerged +} + +export const getGaugeChartDataReviewedPRs = (data) => { + const totalMerged = data.pull_requests.merged + const totalMergedNotByAuthor = data.pull_requests.not_merged_by_author + + const percentatgeReviewed = totalMerged > 0 ? totalMergedNotByAuthor / totalMerged : 0 + + return percentatgeReviewed +} + +export const getGaugeChartDataMergesPRs = (data) => { + const totalMerged = data.pull_requests.merged + const merges = data.commit_merges + + const percentageMergesPR = merges > 0 ? totalMerged / merges : 0 + + return percentageMergesPR +} + +export const getGaugeDataCreatedPRsPerUser = (data) => { + const total = data.pull_requests.total + return Object.entries(data.pull_requests.created).map(([user, created]) => { + return { + user, + percentage: total > 0 ? created / total : 0, + }; + }); +}; + + +export const getGaugeDataMergedPRsPerUser = (data) => { + const totalMerged = data.pull_requests.merged + return Object.entries(data.pull_requests.merged_per_member).map(([user, merged]) => { + return { + user, + percentage: totalMerged > 0 ? merged / totalMerged : 0, + }; + }); +}; + +export const transformCreatedPRsDataForUser = (data, username) => { + const xDataCreatedPRs = []; + const yDataCreatedPRs = []; + + for (const date in data) { + xDataCreatedPRs.push(date); + yDataCreatedPRs.push(data[date].pull_requests?.created?.[username] || 0); + } + + return { xDataCreatedPRs, yDataCreatedPRs }; + }; + +export const transformMergedPRsDataForUser = (data, username) => { + const xDataMergedPRs = []; + const yDataMergedPRs = []; + + for (const date in data) { + xDataMergedPRs.push(date); + yDataMergedPRs.push(data[date].pull_requests?.merged_per_member?.[username] || 0); + } + + return { xDataMergedPRs, yDataMergedPRs }; + }; + +export const transformPRDataForAreaChart = (data) => { + const xDataPRs = []; + const mergedPRs = []; + const closedPRs = []; + const openPRs = []; + + for (const date in data) { + const total = data[date].pull_requests.total || 0; + const merged = data[date].pull_requests.merged || 0; + const closed = data[date].pull_requests.closed || 0; + const open = total - merged - closed; + + xDataPRs.push(date); + mergedPRs.push(merged); + closedPRs.push(closed); + openPRs.push(open); + } + + const areaPRData = [ + { label: 'Merged', data: mergedPRs, color:'rgb(0, 255, 0)' }, + { label: 'Closed', data: closedPRs, color: 'orange' }, + { label: 'Open', data: openPRs, color: 'rgb(255, 0, 0)' }, + ]; + + return { xDataPRs, areaPRData }; +}; + +export const getPieDataPullRequestStatus = (data) => { + const pullRequests = data.pull_requests; + const open = pullRequests.total - pullRequests.merged - pullRequests.closed + const pieDataPullRequestStatus = [ + ["Open", open], + ["Closed", pullRequests.closed], + ["Merged", pullRequests.merged], + ]; + const pieDataPullRequestStatusColor = ["red", "orange","green"]; + return {pieDataPullRequestStatus,pieDataPullRequestStatusColor}; +} \ No newline at end of file diff --git a/src/domain/utils.jsx b/src/domain/utils.jsx new file mode 100644 index 0000000..a1bb271 --- /dev/null +++ b/src/domain/utils.jsx @@ -0,0 +1,21 @@ +export const filterHistoricData = (data, days) => { + if (days === "lifetime") return data; + + const today = new Date(); + const cutoff = new Date(today); + cutoff.setDate(today.getDate() - parseInt(days)); + const cutoffDateString = cutoff.toISOString().split("T")[0]; + + const filtered = {}; + for (const date in data) { + if (date >= cutoffDateString) { + filtered[date] = data[date]; + } + } + + return filtered; +}; + +export const truncateName = (name, maxLength = 18) => { + return name.length > maxLength ? name.slice(0, maxLength) + '...' : name; +}; diff --git a/src/historicalSections/commitsPageHistoricalSection.jsx b/src/historicalSections/commitsPageHistoricalSection.jsx new file mode 100644 index 0000000..12d707b --- /dev/null +++ b/src/historicalSections/commitsPageHistoricalSection.jsx @@ -0,0 +1,37 @@ +// components/historicalSection.jsx +import React from 'react'; +import LineChartMultiple from '../components/lineChartMultiple.jsx'; + +function CommitsPageHistoricalSection({ xDataCommits, seriesDataCommits, xDataModified, seriesDataModified, hasHistoric }) { + if (!hasHistoric) { + return ( +
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop + s'hagi fet la primera execució del + workflow Daily Metrics. +
+ ); + } + + return ( +
+
+
+ +
+
+ +
+
+
+ ); +} + +export default CommitsPageHistoricalSection; diff --git a/src/historicalSections/indexHistoricalSection.jsx b/src/historicalSections/indexHistoricalSection.jsx new file mode 100644 index 0000000..0806dfa --- /dev/null +++ b/src/historicalSections/indexHistoricalSection.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import LineChart from '../components/lineChart'; +import AreaChart from '../components/areaChart'; +import AreaChartMultiple from '../components/areaChartMultiple'; + +function IndexHistoricalSection({ features, historicData, xData, yData, xDataModifedLines, yDataModifedLines, xDataIssues, openIssues, closedIssues, xDataPRs, areaPRData, xDataTask, allSeries }) { + if (!historicData) { + return ( +
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà. +
+ ); + } + + return ( +
+
+ +
+
+ +
+ {features.includes("issues") && ( +
+ +
+ )} + {features.includes("pull-requests") && ( +
+ +
+ )} + {features.includes("projects") && ( +
+ +
+ )} +
+ ); +} + +export default IndexHistoricalSection; diff --git a/src/historicalSections/individualPageHistoricalSection.jsx b/src/historicalSections/individualPageHistoricalSection.jsx new file mode 100644 index 0000000..797d178 --- /dev/null +++ b/src/historicalSections/individualPageHistoricalSection.jsx @@ -0,0 +1,167 @@ +// HistoricalCharts.jsx +import React from 'react'; +import LineChart from '../components/lineChart.jsx'; + +function IndividualHistoricSection({ + historicData, + features, + xDataCommits, + yDataCommits, + xDataModifiedLines, + yDataModifiedLines, + xDataAssignedIssues, + yDataAssignedIssues, + xDataClosedIssues, + yDataClosedIssues, + xDataCreatedPRs, + yDataCreatedPRs, + xDataMergedPRs, + yDataMergedPRs, + xDataTasksAssigned, + yDataTasksAssigned, + xDataTasksToDo, + yDataTasksToDo, + xDataTasksInProgress, + yDataTasksInProgress, + xDataTasksDone, + yDataTasksDone, + xDataTasksStandard, + yDataTasksStandard +}) { + if (!historicData) { + return ( +
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop + s'hagi fet la primera execució del + workflow Daily Metrics. +
+ ); + } + return ( +
+
+ +
+
+ +
+ {features.includes("issues") && ( + +
+ +
)} + {features.includes("issues") && ( +
+ +
)} + {features.includes("pull-requests") && ( +
+ +
)} + {features.includes("pull-requests") && ( +
+ +
)} + {features.includes("projects") && ( +
+ +
+ )} + {features.includes("projects") && ( +
+ +
+ )} + {features.includes("projects") && ( +
+ +
+ )} + {features.includes("projects") && ( +
+ +
+ )} + {features.includes("projects") && ( +
+ +
+ )} +
+ ); +} + +export default IndividualHistoricSection; diff --git a/src/historicalSections/issuesPageHistoricalSection.jsx b/src/historicalSections/issuesPageHistoricalSection.jsx new file mode 100644 index 0000000..a69b05f --- /dev/null +++ b/src/historicalSections/issuesPageHistoricalSection.jsx @@ -0,0 +1,38 @@ +// components/issuesHistoricalSection.jsx +import React from 'react'; +import LineChartMultiple from '../components/lineChartMultiple.jsx'; + +function IssuesPageHistoricalSection({ historicData, xDataAssigned, seriesDataAssigned }) { + if (!historicData) { + return ( +
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop s'hagi fet la primera execució del workflow Daily Metrics. +
+ ); + } + + return ( +
+
+
+ +
+
+
+ ); +} + +export default IssuesPageHistoricalSection; diff --git a/src/historicalSections/projectsPageHistoricalSection.jsx b/src/historicalSections/projectsPageHistoricalSection.jsx new file mode 100644 index 0000000..5162830 --- /dev/null +++ b/src/historicalSections/projectsPageHistoricalSection.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import LineChartMultiple from '../components/lineChartMultiple'; +import AreaChartMultiple from '../components/areaChartMultiple'; + +function ProjectsPageHistoricalSection({ + historicData, + xDataAssigned, + seriesDataAssigned, + xDataFeature, + allSeries, +}) { + if (!historicData) { + return ( +
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop + s'hagi fet la primera execució del + workflow Daily Metrics. +
+ ); + } + + return ( +
+
+
+ +
+
+ +
+
+
+ ); +} + +export default ProjectsPageHistoricalSection; \ No newline at end of file diff --git a/src/historicalSections/pullRequestsPageHistoricalSection.jsx b/src/historicalSections/pullRequestsPageHistoricalSection.jsx new file mode 100644 index 0000000..4cdfe29 --- /dev/null +++ b/src/historicalSections/pullRequestsPageHistoricalSection.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import LineChartMultiple from '../components/lineChartMultiple.jsx'; + +function PullRequestsPageHistoricalSection({ historicData, xDataCreated, seriesDataCreated, xDataMerged, seriesDataMerged }) { + if (!historicData) { + return ( +
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà un cop s'hagi fet la primera execució del workflow Daily Metrics. +
+ ); + } + + return ( +
+
+
+ +
+
+ +
+
+
+ ); +} + +export default PullRequestsPageHistoricalSection; diff --git a/src/metricsSections/commitsPageMetricsSection.jsx b/src/metricsSections/commitsPageMetricsSection.jsx new file mode 100644 index 0000000..db1ff0a --- /dev/null +++ b/src/metricsSections/commitsPageMetricsSection.jsx @@ -0,0 +1,43 @@ +// components/metricsByUserSection.jsx +import React from 'react'; +import GaugeChart from '../components/gaugeChart.jsx'; + +function CommitsPageMetricsSection({ commitsGaugeData, modifiedLinesGaugeData, totalPeople }) { + return ( +
+

Metrics by User

+ +

+ Commits per user + + ⓘ + + Percentage of commits per user relative to the total number of commits + + +

+
+ {commitsGaugeData.map(({ user, percentage }) => ( + + ))} +
+ +

+ Modified lines per user + + ⓘ + + Percentage of modified lines per user relative to the total number of modified lines + + +

+
+ {modifiedLinesGaugeData.map(({ user, percentage }) => ( + + ))} +
+
+ ); +} + +export default CommitsPageMetricsSection; diff --git a/src/metricsSections/indexGraphicsSection.jsx b/src/metricsSections/indexGraphicsSection.jsx new file mode 100644 index 0000000..f4657a1 --- /dev/null +++ b/src/metricsSections/indexGraphicsSection.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import RadarChart from '../components/radarChart'; +import PieChart from '../components/pieChart'; +import GaugeChart from '../components/gaugeChart'; +import LineChart from '../components/lineChart'; + +function IndexGraphicsSection({ features, radarData, pieDataPullRequestStatus, pieDataPullRequestStatusColor, pieDataIssuesStatus, pieDataIssuesStatusColor, pieDataTasksStatus, pieDataTasksStatusColor, historicData, gaugeAnonymousData, xData, yData, xDataModifedLines, yDataModifedLines }) { + const onlyCommitsFallback = ( +
+
+ +
+ {historicData ? ( +
+
+ +
+
+ +
+
+ ) : ( +
+ No s'ha trobat historic_metrics.json.
+ Si és el primer dia, torna demà. +
+ )} +
+ ); + + if (!(features.includes("issues") || features.includes("pull-requests") || features.includes("projects"))) { + return onlyCommitsFallback; + } + + return ( +
+
+ +
+ {features.includes("pull-requests") && ( +
+ +
+ )} + {features.includes("issues") && ( +
+ +
+ )} + {features.includes("projects") && ( +
+ +
+ )} +
+ ); +} + +export default IndexGraphicsSection; diff --git a/src/metricsSections/individualPageMetricsSection.jsx b/src/metricsSections/individualPageMetricsSection.jsx new file mode 100644 index 0000000..c208a9f --- /dev/null +++ b/src/metricsSections/individualPageMetricsSection.jsx @@ -0,0 +1,177 @@ +// UserGaugeSection.jsx +import React from 'react'; +import GaugeChart from '../components/gaugeChart.jsx'; + +function IndividualMetricsSection({ + selectedUser, + totalPeople, + percentageCommits, + percentageModifiedlLines, + percentageAssigned, + percentageIssuesClosed, + percentageCreated, + percentageMerged, + percentageTasksAssigned, + percentageTasksInProgress, + percentageTasksDone, + percentageTasksStandard, + features +}) { + return (
+
+

+ Commits + + ⓘ + Percentage of commits made by the user relative to the total number of commits + +

+ +
+
+

+ Modified Lines + + ⓘ + Percentage of commits made by the user relative to the total number of commits + +

+ +
+ {features.includes("issues") && ( +
+

+ Issues assigned + + ⓘ + Percentage of issues assigned to the user relative to the number of assigned issues + +

+ +
)} + {features.includes("issues") && ( +
+

+ Issues closed + + ⓘ + Percentage of issues closed by the user relative to the issues assigned to the user + +

+ +
)} + {features.includes("pull-requests") && ( +
+

+ Pull Requests created + + ⓘ + Percentage of pull requests created by the user relative to the total number of pull requests + +

+ +
)} + {features.includes("pull-requests") && ( +
+

+ Pull Requests merged + + ⓘ + Percentage of pull requests merged by the user relative to the total number of pull requests merged + +

+ +
)} + {features.includes("projects") && ( +
+

+ Tasks assigned + + ⓘ + Percentage of tasks assigned to the user relative to the number of assigned tasks + +

+ +
)} + {features.includes("projects") && ( +
+

+ Tasks In Progress + + ⓘ + + Shows if the user is actively working on at least one task. + If the gauge is at 100%, the user has at least one task in progress. + If it's at 0%, the user currently has no tasks in progress. + + +

+ +
)} + {features.includes("projects") && ( +
+

+ Tasks Done + + ⓘ + Percentage of tasks done by the user relative to the tasks assigned to the user + +

+ +
)} + {features.includes("projects") && ( +
+

+ Tasks with standard status + + ⓘ + Percentage of tasks with standard status (ToDo, In Progress, Done) of the user relative to the user assigned tasks + +

+ +
)} +
+ ); +} + +export default IndividualMetricsSection; \ No newline at end of file diff --git a/src/metricsSections/issuesPageMetricsSection.jsx b/src/metricsSections/issuesPageMetricsSection.jsx new file mode 100644 index 0000000..81d319a --- /dev/null +++ b/src/metricsSections/issuesPageMetricsSection.jsx @@ -0,0 +1,53 @@ +// components/issuesMetricsByUserSection.jsx +import React from 'react'; +import GaugeChart from '../components/gaugeChart.jsx'; + +function IssuesPageMetricsSection({ gaugeDataAssigned, gaugeDataClosed, totalPeople }) { + return ( +
+

Metrics by User

+ +

+ Issues assigned per user + + ⓘ + + Percentage of issues assigned per user relative to the number of assigned issues + + +

+
+ {gaugeDataAssigned.map(({ user, percentage }) => ( + + ))} +
+ +

+ Issues closed per user + + ⓘ + + Percentage of issues closed per user relative to the issues assigned to that user + + +

+
+ {gaugeDataClosed.map(({ user, percentage }) => ( + + ))} +
+
+ ); +} + +export default IssuesPageMetricsSection; diff --git a/src/metricsSections/projectsPageMetricsSection.jsx b/src/metricsSections/projectsPageMetricsSection.jsx new file mode 100644 index 0000000..09727c5 --- /dev/null +++ b/src/metricsSections/projectsPageMetricsSection.jsx @@ -0,0 +1,98 @@ +import React from 'react'; +import GaugeChart from '../components/gaugeChart'; + +function ProjectsPageMetricsSection({ + gaugeDataAssignedTasks, + gaugeDataInProgressTasks, + gaugeDataDoneTasks, + gaugeDataStandardTasks, + totalPeople, +}) { + return ( +
+

Metrics by User

+ +

+ Tasks assigned per user + + ⓘ + Percentage of tasks assigned per user relative to the number of assigned tasks + +

+
+ {gaugeDataAssignedTasks.map(({ user, percentage }) => ( + + ))} +
+ +

+ Tasks In Progress per user + + ⓘ + + Shows if the user is actively working on at least one task. + If the gauge is at 100%, the user has at least one task in progress. + If it's at 0%, the user currently has no tasks in progress. + + +

+
+ {gaugeDataInProgressTasks.map(({ user, percentage }) => ( + + ))} +
+ +

+ Tasks Done per user + + ⓘ + + Percentage of tasks done per user relative to the tasks assigned to that user + + +

+
+ {gaugeDataDoneTasks.map(({ user, percentage }) => ( + + ))} +
+ +

+ Tasks with standard status per user + + ⓘ + + Percentage of tasks with standard status (ToDo, In Progress, Done) per user relative to their assigned tasks + + +

+
+ {gaugeDataStandardTasks.map(({ user, percentage }) => ( + + ))} +
+
+ ); +} + +export default ProjectsPageMetricsSection; \ No newline at end of file diff --git a/src/metricsSections/pullRequestsPageMetricsSection.jsx b/src/metricsSections/pullRequestsPageMetricsSection.jsx new file mode 100644 index 0000000..33ce11d --- /dev/null +++ b/src/metricsSections/pullRequestsPageMetricsSection.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import GaugeChart from '../components/gaugeChart.jsx'; + +function PullRequestsMetricsSection({ gaugeDataCreatedPRs, gaugeDataMergedPRs, totalPeople }) { + return ( +
+

Metrics by User

+ +

+ Pull requests created per user + + ⓘ + + Percentage of pull requests created per user relative to the total number of pull requests + + +

+
+ {gaugeDataCreatedPRs.map(({ user, percentage }) => ( + + ))} +
+ +

+ Pull requests merged per user + + ⓘ + + Percentage of pull requests merged per user relative to the number of merged pull requests + + +

+
+ {gaugeDataMergedPRs.map(({ user, percentage }) => ( + + ))} +
+
+ ); +} + +export default PullRequestsMetricsSection; diff --git a/src/pages/commitsPage.jsx b/src/pages/commitsPage.jsx index 2411c74..7ecf644 100644 --- a/src/pages/commitsPage.jsx +++ b/src/pages/commitsPage.jsx @@ -1,152 +1,78 @@ import React from 'react'; -import GaugeChart from '../components/gaugeChart.jsx'; -import RadarPieToggle from '../components/radarPieToggle.jsx'; -import LineChartMultiple from '../components/lineChartMultiple.jsx'; import usePersistentStateSession from '../components/usePersistentStateSession.jsx'; -import '../styles/commits.css'; +import HistoricalToggle from '../components/historicalToggle.jsx'; +import DateRangeSelector from '../components/dateRangeSelector.jsx'; +import CommitsPageSummarySection from '../summarySections/commitsPageSummarySection.jsx'; +import CommitsPageMetricsSection from '../metricsSections/commitsPageMetricsSection.jsx'; +import CommitsPageHistoricalSection from '../historicalSections/commitsPageHistoricalSection.jsx'; +import '../styles/elements.css'; import { - filterHistoricData, transformCommitsDataForLineChart, transformModifiedLinesDataForLineChart, - prepareRadarData, - getPieChartData, - getGaugeChartPercentages, - getGaugeChartModifiedLines + getPieChartDataCommits, + getPieChartDataModifiedLines, + getGaugeChartDataCommits, + getGaugeChartDataModifiedLines, + GetRadarDataCommits, + GetRadarDataModifiedLines, } from '../domain/commits.jsx'; -function CommitsPage({ data, historicData, features }) { - const commitsData = data.commits; - const modifiedLinesData = data.modified_lines; +import { filterHistoricData } from '../domain/utils.jsx'; +function CommitsPage({ data, historicData, features }) { const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalCommits', false); const [dateRange, setDateRange] = usePersistentStateSession('dateRangeCommits', "7"); - const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null; - const { xDataCommits, seriesDataCommits } = transformCommitsDataForLineChart(filteredhistoricaData); - const { xDataModified, seriesModified } = transformModifiedLinesDataForLineChart(filteredhistoricaData); - const radarChartModifiedLines = prepareRadarData(modifiedLinesData); + const filteredHistoricData = historicData ? filterHistoricData(historicData, dateRange) : null; + + const { xData: xDataCommits, seriesData: seriesDataCommits } = transformCommitsDataForLineChart(filteredHistoricData || {}); + const { xData: xDataModified, seriesData: seriesDataModified } = transformModifiedLinesDataForLineChart(filteredHistoricData || {}); - const totalCommits = commitsData.total; - const totalModifiedLines = modifiedLinesData.total.modified; - const totalPeople = Object.keys(data.avatars).length; + const totalCommits = data.commits?.total || 0; + const gaugeDataAnonymous = totalCommits > 0 ? ((totalCommits - (data.commits?.anonymous || 0)) / totalCommits) : 0; - const dataPieChartCommits = getPieChartData(commitsData); - const dataPieChartModifiedLines = getPieChartData(modifiedLinesData, 'modified'); - const commitsGaugeData = getGaugeChartPercentages(commitsData, 'total', 'anonymous'); - const modifiedLinesGaugeData = getGaugeChartModifiedLines(modifiedLinesData, 'total'); + const dataPieChartCommits = getPieChartDataCommits(data); + const dataPieChartModifiedLines = getPieChartDataModifiedLines(data); + const commitsGaugeData = getGaugeChartDataCommits(data); + const modifiedLinesGaugeData = getGaugeChartDataModifiedLines(data); + const radarChartCommits = GetRadarDataCommits(data); + const radarChartModifiedLines = GetRadarDataModifiedLines(data); + + const totalPeople = Object.keys(data.avatars || {}).length || 1; return (

Commits

-
-
- - -
-
- {showHistorical && ( -
- -
- )} + + {!showHistorical && ( <> -
-

Summary

-
-
- -
-
- -
-
-

- Non-anonymous commits - - ⓘ - Percentage of commits that have a member of the project as its author - -

- 0 ? (totalCommits - commitsData.anonymous) / totalCommits : 0} - totalPeople={1} - /> -
-
-
- -
-

Metrics by User

-

- Commits per user - - ⓘ - Percentage of commits per user relative to the total number of commits - -

-
- {commitsGaugeData.map(({ user, percentage }) => ( - - ))} -
- -

- Modified lines per user - - ⓘ - Percentage of modified lines per user relative to the total number of modified lines - -

-
- {modifiedLinesGaugeData.map(({ user, percentage }) => ( - - ))} -
-
+ + )} {showHistorical && ( - <> - {historicData ? ( -
-
-
- -
-
- -
-
-
- ) : ( -
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop s'hagi fet la primera execució del workflow Daily Metrics. -
- )} - + )}
); diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 909580e..6a6f238 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,472 +1,140 @@ -import React, { useState } from 'react'; -import "../styles/index.css"; -import PieChart from '../components/pieChart'; -import RadarChart from '../components/radarChart'; -import LineChart from '../components/lineChart'; -import AreaChart from '../components/areaChart'; -import GaugeChart from '../components/gaugeChart'; -import AreaChartMultiple from '../components/areaChartMultiple'; -import usePersistentStateSession from '../components/usePersistentStateSession'; - -function Index({data,historicData,features}) { +import React from 'react'; +import "../styles/index.css"; +import usePersistentStateSession from '../components/usePersistentStateSession'; +import HistoricalToggle from '../components/historicalToggle.jsx'; +import DateRangeSelector from '../components/dateRangeSelector.jsx'; + +import IndexStatsSection from '../summarySections/indexStatsSection.jsx'; +import IndexGraphicsSection from '../metricsSections/indexGraphicsSection.jsx'; +import IndexHistoricalSection from '../historicalSections/indexHistoricalSection.jsx'; + +import { filterHistoricData } from '../domain/utils'; +import { + getGaugeDataAnonymous, + transformDataForLineChartCommits, + transformDataForLineChartModifiedLines +} from '../domain/commits'; +import { + getGaugeDataIssuesAssigned, + getGaugeDataIssuesHavePR, + transformIssuesDataForAreaChart, + getPieDataIssuesStatus +} from '../domain/issues'; +import { + getGaugeChartDataMergedPRs, + getGaugeChartDataReviewedPRs, + getGaugeChartDataMergesPRs, + transformPRDataForAreaChart, + getPieDataPullRequestStatus +} from '../domain/pullRequests'; +import { + getGaugeDataTasksAssigned, + getGaugeDataTasksStandardStatus, + getGaugeDataItemsIssues, + getGaugeDataItemIssuesWithType, + getGaugeDataItemIssuesWithIteration, + transformTaskDataForAreaChart, + getPieDataTasksStatus +} from '../domain/projects'; + +function Index({ data, historicData, features }) { const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIndex', false); const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIndex', "7"); - const pullRequests = data.pull_requests; - const open = pullRequests.total - pullRequests.merged - pullRequests.closed - const issues = data.issues - const datapullRequests = [ - ["Open", open], - ["Closed", pullRequests.closed], - ["Merged", pullRequests.merged], - ]; - const dataissues = [ - ["Open",issues.total - issues.total_closed], - ["Closed",issues.total_closed] - ] - const filterHistoricData = (data, days) => { - if (days === "lifetime") return data; - - const today = new Date(); - const cutoff = new Date(today); - cutoff.setDate(today.getDate() - parseInt(days)); - const cutoffDateString = cutoff.toISOString().split("T")[0]; - const filtered = {}; - for (const date in data) { - if (date >= cutoffDateString) { - filtered[date] = data[date]; - } - } - return filtered; - }; const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null; - const taskData = data.project.metrics_by_iteration.total; - const totalTasks = taskData.total; - const totalInProgress = taskData.in_progress; - const totalDone = taskData.done; - const totalToDo = taskData.todo; - const dataTasks = [ - ["Todo",totalToDo], - ["In Progress",totalInProgress], - ["Done", totalDone] - ] - - for (const [key, value] of Object.entries(taskData)) { - if ( - !["todo", "in_progress", "done", "total"].includes(key) && - !key.startsWith("total_") && - !key.startsWith("total") && - typeof value === "number" && - Number.isInteger(value) - ) { - const label = key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); - dataTasks.push([label, value]); - } - } - const colorsPR = ["red", "orange","green"]; - const colorsIssues = ["red","green"]; - const colorsProject = ["red", "orange","green"]; + // Radar Data let radarData = {}; - - radarData['Non-Anonymous Commits'] = - data.commits.total > 0 ? (data.commits.total - data.commits.anonymous) / data.commits.total : 0; + radarData['Non-Anonymous Commits'] = getGaugeDataAnonymous(data); if (features.includes('issues')) { - radarData['Issues Assigned'] = - data.issues.total > 0 ? (data.issues.total - data.issues.assigned.non_assigned) / data.issues.total : 0; - - radarData['Issues associated PR'] = - data.issues.total_closed > 0 ? data.issues.have_pull_request / data.issues.total_closed : 0; + radarData['Issues Assigned'] = getGaugeDataIssuesAssigned(data); + radarData['Issues associated PR'] = getGaugeDataIssuesHavePR(data); } if (features.includes('pull-requests')) { - radarData['Pull Requests Merged'] = - (data.pull_requests.total - data.pull_requests.closed) > 0 ? data.pull_requests.merged / (data.pull_requests.total - data.pull_requests.closed) : 0; - - radarData['Pull Requests Reviewed'] = - data.pull_requests.merged > 0 ? data.pull_requests.not_merged_by_author / data.pull_requests.merged : 0; - - radarData['Pull Requests Merges'] = - data.commit_merges > 0 ? data.pull_requests.merged / data.commit_merges : 0; + radarData['Pull Requests Merged'] = getGaugeChartDataMergedPRs(data); + radarData['Pull Requests Reviewed'] = getGaugeChartDataReviewedPRs(data); + radarData['Pull Requests Merges'] = getGaugeChartDataMergesPRs(data); } - if (features.includes('projects')) { - const taskData = data.project.metrics_by_iteration.total; - const { non_assigned, ...assignedPerMember } = taskData.assigned_per_member; - const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0); - const totalTasks = taskData.total_tasks; - radarData['Tasks Assigned'] = - totalTasks > 0 ? totalAssigned / totalTasks : 0 - const todo = taskData.todo - const inProgress = taskData.in_progress - const done = taskData.done - const standard = todo + inProgress + done; - radarData['Tasks With Standard Status'] = - totalTasks > 0 ? standard / totalTasks : 0 + if (features.includes('projects')) { + radarData['Tasks Assigned'] = getGaugeDataTasksAssigned(data, "total"); + radarData['Tasks With Standard Status'] = getGaugeDataTasksStandardStatus(data, "total"); + radarData['Items that are Issues'] = getGaugeDataItemsIssues(data, "total"); + radarData['Projects Issues with type'] = getGaugeDataItemIssuesWithType(data, "total"); - const totalDraftIssues = taskData.total - const totalIssues = taskData.total_issues - radarData['Items that are Issues'] = - totalDraftIssues > 0 ? totalIssues / totalDraftIssues : 0 - const IssuesWithType = taskData.total_issues_with_type - radarData['Projects Issues with type'] = - totalIssues > 0 ? IssuesWithType / totalIssues : 0 - if(data.project.has_iterations) { - const noIterationDraftIssues = data.project.metrics_by_iteration.no_iteration.total - const iterationDraftIssues = totalDraftIssues - noIterationDraftIssues - radarData['Items with iteration'] = - totalDraftIssues > 0 ? iterationDraftIssues / totalDraftIssues : 0 + if (data.project.has_iterations) { + radarData['Items with iteration'] = getGaugeDataItemIssuesWithIteration(data, "total"); } } - const transformDataForLineChart = (data) => { - const xData = []; - const yData = []; - for (const date in data) { - xData.push(date); - yData.push(data[date].commits.total); - } - - return { xData, yData }; - }; - const { xData, yData } = transformDataForLineChart(filteredhistoricaData); + // Pie Chart Data + const { pieDataPullRequestStatus, pieDataPullRequestStatusColor } = getPieDataPullRequestStatus(data); + const { pieDataIssuesStatus, pieDataIssuesStatusColor } = getPieDataIssuesStatus(data); + const { pieDataTasksStatus, pieDataTasksStatusColor } = getPieDataTasksStatus(data); - const transformDataForLineChartModifiedLines = (data) => { - const xDataModifedLines = []; - const yDataModifedLines = []; - for (const date in data) { - xDataModifedLines.push(date); - yDataModifedLines.push(data[date].modified_lines.total.modified); - } - - return { xDataModifedLines, yDataModifedLines }; - }; + // Line/Area Chart Data + const { xData, yData } = transformDataForLineChartCommits(filteredhistoricaData); const { xDataModifedLines, yDataModifedLines } = transformDataForLineChartModifiedLines(filteredhistoricaData); - - const transformIssuesDataForAreaChart = (data) => { - const xDataIssues = []; - const closedIssues = []; - const openIssues = []; - - for (const date in data) { - const total = data[date].issues.total || 0; - const closed = data[date].issues.total_closed || 0; - const open = total - closed; - xDataIssues.push(date); - closedIssues.push(closed); - openIssues.push(open); - } - return { xDataIssues, closedIssues, openIssues }; - }; const { xDataIssues, closedIssues, openIssues } = transformIssuesDataForAreaChart(filteredhistoricaData); - const transformPRDataForAreaChart = (data) => { - const xDataPRs = []; - const mergedPRs = []; - const openPRs = []; - - for (const date in data) { - const total = data[date].pull_requests.total || 0; - const merged = data[date].pull_requests.merged || 0; - const closed = data[date].pull_requests.closed || 0; - const open = total - merged - closed; - xDataPRs.push(date); - mergedPRs.push(merged); - openPRs.push(open); - } - return { xDataPRs, mergedPRs, openPRs }; - }; - const { xDataPRs, mergedPRs, openPRs } = transformPRDataForAreaChart(filteredhistoricaData); - - const transformTaskDataForAreaChart = (data) => { - const xDataTask = []; - const doneTask = []; - const inProgressTask = []; - const toDoTask = [] - const otherKeysData = {} - const allOtherKeys = new Set(); - - for (const date in data) { - const metrics = data[date].project.metrics_by_iteration.total; - for (const [key, value] of Object.entries(metrics)) { - if ( - !['done', 'in_progress', 'todo', 'total'].includes(key) && - !key.startsWith('total_') && - !key.startsWith('total') && - typeof value === 'number' && - Number.isInteger(value) - ) { - allOtherKeys.add(key); - } - } - } - - for (const key of allOtherKeys) { - otherKeysData[key] = []; - } - for (const date in data) { - const metrics = data[date].project.metrics_by_iteration.total; - const done = metrics.done || 0; - const todo = metrics.todo || 0; - const inProgress = metrics.in_progress || 0; + const { xDataPRs, areaPRData } = transformPRDataForAreaChart(filteredhistoricaData); + const { xDataTask, allSeries } = transformTaskDataForAreaChart(filteredhistoricaData); - xDataTask.push(date); - doneTask.push(done); - inProgressTask.push(inProgress); - toDoTask.push(todo) - for (const key of allOtherKeys) { - const value = metrics[key]; - otherKeysData[key].push(Number.isInteger(value) ? value : 0); - } - } - return { xDataTask, doneTask, inProgressTask,toDoTask,otherKeysData }; - }; - const { xDataTask, doneTask, inProgressTask,toDoTask,otherKeysData } = transformTaskDataForAreaChart(filteredhistoricaData); - - const baseSeries = [ - { label: 'Done', data: doneTask, color: "rgb(0, 255, 0)" }, - { label: 'In Progress', data: inProgressTask, color: 'orange' }, - { label: 'To Do', data: toDoTask, color: "rgb(255, 0, 0)" } - ]; + const gaugeAnonymousData = getGaugeDataAnonymous(data); - const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({ - label: key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), - data, - })); - - const allSeries = [...baseSeries, ...otherSeries]; - - return (

General overview

-
-
-
-

Project Stats

-
-
-
Total Members: {Object.keys(data.commits).filter(user => user !== 'anonymous' && user !== 'total').length}
-
Total Commits: {data.commits.total}
- {features.includes("issues") && ( -
Total Issues: {data.issues.total}
) } - {features.includes("pull-requests") && ( -
Total Pull Requests: {data.pull_requests.total}
)} -
Total Additions: {data.modified_lines.total.additions}
-
Total Deletions: {data.modified_lines.total.deletions}
-
Total Modifications: {data.modified_lines.total.modified}
-
Total Lines of code: {data.modified_lines.total.additions - data.modified_lines.total.deletions}
- {features.includes("projects") && ( -
Total Tasks: {data.project.metrics_by_iteration.total.total_tasks}
)} - {features.includes("projects") && ( -
Total Features: {data.project.metrics_by_iteration.total.total_features}
)} -
-
-
- {(features.includes("issues") || features.includes("pull-requests") || features.includes("projects")) && ( -
-
- - -
-
)} - - {showHistorical && ( -
- -
- )} - - {!showHistorical && ( - <> - {(features.includes("issues") || features.includes("pull-requests") || features.includes("projects")) ? ( -
-
- -
- {features.includes("pull-requests") && ( -
- -
- )} - {features.includes("issues") && ( -
- -
- )} - {features.includes("projects") && ( -
- -
- )} -
- ) : ( -
-
-
- 0 ? (data.commits.total - data.commits.anonymous) / data.commits.total : 0 } - totalPeople={1} - /> -
- { historicData ? - - (
-
- -
-
- -
-
) :
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop
- s'hagi fet la primera execució del
- workflow Daily Metrics. -
} -
-
- )} - -)} - - {showHistorical && ( -
- {historicData ? ( -
-
- -
-
- -
- {features.includes("issues") && ( -
- -
)} - {features.includes("pull-requests") && ( -
- -
)} - {features.includes("projects") && ( -
- -
)} -
- ) : ( -
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop - s'hagi fet la primera execució del - workflow Daily Metrics. -
- )} -
+ + + {(features.includes("issues") || features.includes("pull-requests") || features.includes("projects")) && ( + + )} + + + + {!showHistorical ? ( + + ) : ( + )}
- ); + ); } export default Index; diff --git a/src/pages/individual.jsx b/src/pages/individual.jsx deleted file mode 100644 index b0a811d..0000000 --- a/src/pages/individual.jsx +++ /dev/null @@ -1,593 +0,0 @@ -import React, { useState } from 'react'; -import "../styles/individual.css"; -import LineChart from '../components/lineChart'; -import GaugeChart from '../components/gaugeChart'; -import usePersistentStateSession from '../components/usePersistentStateSession'; -import usePersistentState from '../components/usePersistentState'; - -function Individual({ data, historicData, features }) { - const [selectedUser, setSelectedUser] = usePersistentState("selectedUser",Object.keys(data.avatars)[0]); - const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIndividual', false); - const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIndividual', "7"); - const filterHistoricData = (data, days) => { - if (days === "lifetime") return data; - - const today = new Date(); - const cutoff = new Date(today); - cutoff.setDate(today.getDate() - parseInt(days)); - const cutoffDateString = cutoff.toISOString().split("T")[0]; - const filtered = {}; - for (const date in data) { - if (date >= cutoffDateString) { - filtered[date] = data[date]; - } - } - - return filtered; - }; - const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null; - const users = Object.keys(data.avatars); - const avatar = data.avatars[selectedUser] || ""; - const commits = data.commits[selectedUser] || 0; - const modifiedLines = data.modified_lines[selectedUser] || { additions: 0, deletions: 0, modified: 0 }; - const streak = data.commit_streak[selectedUser] || 0; - const longestStreak = data.longest_commit_streak_per_user[selectedUser] || 0; - const issuesAssigned = data.issues.assigned[selectedUser] || 0; - const issuesClosed = data.issues.closed[selectedUser] || 0; - const totalCommits = data.commits['total'] - const totalModifiedLines = data.modified_lines['total'].modified - const totalPeople = Object.keys(data.avatars).length; - const tasksAssigned = data.project.metrics_by_iteration.total.assigned_per_member[selectedUser] || 0 - const tasksClosed = data.project.metrics_by_iteration.total.done_per_member[selectedUser] || 0 - const tasksInProgress = data.project.metrics_by_iteration.total.in_progress_per_member[selectedUser] || 0 - const tasksTodo = data.project.metrics_by_iteration.total.todo_per_member[selectedUser] || 0 - const tasksStandard = tasksClosed + tasksInProgress + tasksTodo - const truncateName = (name, maxLength = 18) => { - return name.length > maxLength ? name.slice(0, maxLength) + '...' : name; - }; - const userCommits = data.commits[selectedUser]; - const percentageCommits = totalCommits > 0 ? userCommits / totalCommits: 0; - const userModifiedLines = data.modified_lines[selectedUser].modified - const percentageModifiedlLines = totalModifiedLines > 0 ? userModifiedLines / totalModifiedLines : 0 - const totalAssignedIssues = data.issues.total - data.issues.assigned["non_assigned"] - const pullRequestsCreated = data.pull_requests.created[selectedUser] - const pullRequestsMerged = data.pull_requests.merged_per_member[selectedUser] - const percentageAssigned = totalAssignedIssues > 0 ? issuesAssigned / totalAssignedIssues: 0 - const percentageIssuesClosed = issuesAssigned > 0 ? issuesClosed/issuesAssigned: 0 - const percentageCreated = data.pull_requests.total > 0 ? data.pull_requests.created[selectedUser] / data.pull_requests.total: 0 - const percentageMerged = data.pull_requests.merged > 0 ? data.pull_requests.merged_per_member[selectedUser] / data.pull_requests.merged: 0 - const { non_assigned, ...assignedPerMember } = data.project.metrics_by_iteration.total.assigned_per_member; - const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0); - const percentageTasksAssigned = totalAssigned > 0 ? tasksAssigned / totalAssigned : 0; - const percentageTasksDone = tasksAssigned > 0 ? tasksClosed / tasksAssigned : 0; - const percentageTasksInProgress = tasksInProgress > 0 ? tasksInProgress / tasksInProgress : 0; - const percentageTasksStandard = tasksAssigned > 0 ? tasksStandard / tasksAssigned : 0; - const transformCommitsDataForUser = (data, username) => { - const xDataCommits = []; - const yDataCommits = []; - - for (const date in data) { - xDataCommits.push(date); - yDataCommits.push(data[date].commits[username] || 0); - } - - return { xDataCommits, yDataCommits }; - }; - - const { xDataCommits, yDataCommits } = transformCommitsDataForUser(filteredhistoricaData, selectedUser) - - const transformModifiedLinesDataForUser = (data, username) => { - const xDataModifiedLines = []; - const yDataModifiedLines = []; - - for (const date in data) { - const userData = data[date].modified_lines[username].modified; - xDataModifiedLines.push(date); - yDataModifiedLines.push(userData); - } - - return { xDataModifiedLines, yDataModifiedLines }; - }; - const { xDataModifiedLines, yDataModifiedLines } = transformModifiedLinesDataForUser(filteredhistoricaData, selectedUser) - - const transformAssignedIssuesDataForUser = (data, username) => { - const xDataAssignedIssues = []; - const yDataAssignedIssues = []; - - for (const date in data) { - xDataAssignedIssues.push(date); - yDataAssignedIssues.push(data[date].issues?.assigned?.[username] || 0); - } - - return { xDataAssignedIssues, yDataAssignedIssues }; - }; - const { xDataAssignedIssues, yDataAssignedIssues } = transformAssignedIssuesDataForUser(filteredhistoricaData, selectedUser); - - const transformClosedIssuesDataForUser = (data, username) => { - const xDataClosedIssues = []; - const yDataClosedIssues = []; - - for (const date in data) { - xDataClosedIssues.push(date); - yDataClosedIssues.push(data[date].issues?.closed?.[username] || 0); - } - - return { xDataClosedIssues, yDataClosedIssues }; - }; - - const { xDataClosedIssues, yDataClosedIssues } = transformClosedIssuesDataForUser(filteredhistoricaData, selectedUser); - - - const transformCreatedPRsDataForUser = (data, username) => { - const xDataCreatedPRs = []; - const yDataCreatedPRs = []; - - for (const date in data) { - xDataCreatedPRs.push(date); - yDataCreatedPRs.push(data[date].pull_requests?.created?.[username] || 0); - } - - return { xDataCreatedPRs, yDataCreatedPRs }; - }; - const { xDataCreatedPRs, yDataCreatedPRs } = transformCreatedPRsDataForUser(filteredhistoricaData, selectedUser); - - const transformMergedPRsDataForUser = (data, username) => { - const xDataMergedPRs = []; - const yDataMergedPRs = []; - - for (const date in data) { - xDataMergedPRs.push(date); - yDataMergedPRs.push(data[date].pull_requests?.merged_per_member?.[username] || 0); - } - - return { xDataMergedPRs, yDataMergedPRs }; - }; - const { xDataMergedPRs, yDataMergedPRs } = transformMergedPRsDataForUser(filteredhistoricaData, selectedUser); - const transformTasksAssignedDataForUser = (data, username) => { - const xDataTasksAssigned = []; - const yDataTasksAssigned = []; - - for (const date in data) { - xDataTasksAssigned.push(date); - const assigned = data[date]?.project?.metrics_by_iteration?.total?.assigned_per_member?.[username] || 0; - yDataTasksAssigned.push(assigned); - } - - return { xDataTasksAssigned, yDataTasksAssigned }; - }; - - const { xDataTasksAssigned, yDataTasksAssigned } = transformTasksAssignedDataForUser(filteredhistoricaData, selectedUser); - - - const transformTasksToDoDataForUser = (data, username) => { - const xDataTasksToDo = []; - const yDataTasksToDo = []; - - for (const date in data) { - xDataTasksToDo.push(date); - yDataTasksToDo.push(data[date].project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0); - } - - return { xDataTasksToDo, yDataTasksToDo }; - }; - - const { xDataTasksToDo, yDataTasksToDo } = transformTasksToDoDataForUser(filteredhistoricaData, selectedUser); - - const transformTasksInProgressDataForUser = (data, username) => { - const xDataTasksInProgress = []; - const yDataTasksInProgress = []; - - for (const date in data) { - xDataTasksInProgress.push(date); - yDataTasksInProgress.push(data[date].project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0); - } - - return { xDataTasksInProgress, yDataTasksInProgress }; - }; - - const { xDataTasksInProgress, yDataTasksInProgress } = transformTasksInProgressDataForUser(filteredhistoricaData, selectedUser); - - const transformTasksDoneDataForUser = (data, username) => { - const xDataTasksDone = []; - const yDataTasksDone = []; - - for (const date in data) { - xDataTasksDone.push(date); - const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0; - yDataTasksDone.push(done); - } - - return { xDataTasksDone, yDataTasksDone }; - }; - - const { xDataTasksDone, yDataTasksDone } = transformTasksDoneDataForUser(filteredhistoricaData, selectedUser); - - const transformTasksStandardDataForUser = (data, username) => { - const xDataTasksStandard = []; - const yDataTasksStandard = []; - - for (const date in data) { - xDataTasksStandard.push(date); - const todo = data[date]?.project?.metrics_by_iteration?.total?.todo_per_member?.[username] || 0; - const inProgress = data[date]?.project?.metrics_by_iteration?.total?.in_progress_per_member?.[username] || 0; - const done = data[date]?.project?.metrics_by_iteration?.total?.done_per_member?.[username] || 0; - yDataTasksStandard.push(todo + inProgress + done); - } - - return { xDataTasksStandard, yDataTasksStandard }; - }; - - const { xDataTasksStandard, yDataTasksStandard } = transformTasksStandardDataForUser(filteredhistoricaData, selectedUser); - - return ( -
-

Individual overview

-
-
-
- {selectedUser} -
-
-
Commits: {commits}
-
Additions: {modifiedLines.additions}
-
Deletions: {modifiedLines.deletions}
-
Modifications: {modifiedLines.modified}
-
Commit Streak: {streak}
-
Longest Streak: {longestStreak}
- {features.includes("issues") && ( -
Issues Assigned: {issuesAssigned}
)} - {features.includes("issues") && ( -
Issues Closed: {issuesClosed}
)} - {features.includes("pull-requests") && ( -
Pull requests Created: {pullRequestsCreated}
)} - {features.includes("pull-requests") && ( -
Pull requests merged: {pullRequestsMerged}
)} - {features.includes("projects") && ( -
Tasks assigned: {tasksAssigned}
)} - {features.includes("projects") && ( -
Tasks todo: {tasksTodo}
)} - {features.includes("projects") && ( -
Tasks in progress: {tasksInProgress}
)} - {features.includes("projects") && ( -
Tasks done: {tasksClosed}
)} - {features.includes("projects") && ( -
Tasks with Standard Status: {tasksStandard}
)} -
-
-
- -
-
- - -
-
- - {showHistorical && ( -
- -
- )} - - {!showHistorical && ( - <> -
-
-

- Commits - - ⓘ - Percentage of commits made by the user relative to the total number of commits - -

- -
-
-

- Modified Lines - - ⓘ - Percentage of commits made by the user relative to the total number of commits - -

- -
- {features.includes("issues") && ( -
-

- Issues assigned - - ⓘ - Percentage of issues assigned to the user relative to the number of assigned issues - -

- -
)} - {features.includes("issues") && ( -
-

- Issues closed - - ⓘ - Percentage of issues closed by the user relative to the issues assigned to the user - -

- -
)} - {features.includes("pull-requests") && ( -
-

- Pull Requests created - - ⓘ - Percentage of pull requests created by the user relative to the total number of pull requests - -

- -
)} - {features.includes("pull-requests") && ( -
-

- Pull Requests merged - - ⓘ - Percentage of pull requests merged by the user relative to the total number of pull requests merged - -

- -
)} - {features.includes("projects") && ( -
-

- Tasks assigned - - ⓘ - Percentage of tasks assigned to the user relative to the number of assigned tasks - -

- -
)} - {features.includes("projects") && ( -
-

- Tasks In Progress - - ⓘ - - Shows if the user is actively working on at least one task. - If the gauge is at 100%, the user has at least one task in progress. - If it's at 0%, the user currently has no tasks in progress. - - -

- -
)} - {features.includes("projects") && ( -
-

- Tasks Done - - ⓘ - Percentage of tasks done by the user relative to the tasks assigned to the user - -

- -
)} - {features.includes("projects") && ( -
-

- Tasks with Standard Status - - ⓘ - Percentage of tasks with standard status (ToDo, In Progress, Done) of the user relative to the user assigned tasks - -

- -
)} -
- )} - {showHistorical && ( - <> - {historicData ? ( - <> -
-
- -
-
- -
- {features.includes("issues") && ( - -
- -
)} - {features.includes("issues") && ( -
- -
)} - {features.includes("pull-requests") && ( -
- -
)} - {features.includes("pull-requests") && ( -
- -
)} - {features.includes("projects") && ( -
- -
- )} - {features.includes("projects") && ( -
- -
- )} - {features.includes("projects") && ( -
- -
- )} - {features.includes("projects") && ( -
- -
- )} - - {features.includes("projects") && ( -
- -
- )} -
- ) : ( -
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop - s'hagi fet la primera execució del - workflow Daily Metrics. -
)} - - )} -
- ); -} - -export default Individual; diff --git a/src/pages/individualPage.jsx b/src/pages/individualPage.jsx new file mode 100644 index 0000000..d9c75ea --- /dev/null +++ b/src/pages/individualPage.jsx @@ -0,0 +1,202 @@ +import React, { useState } from 'react'; +import "../styles/individual.css"; +import LineChart from '../components/lineChart.jsx'; +import GaugeChart from '../components/gaugeChart.jsx'; +import usePersistentStateSession from '../components/usePersistentStateSession.jsx'; +import usePersistentState from '../components/usePersistentState.jsx'; +import HistoricalToggle from '../components/historicalToggle.jsx'; +import DateRangeSelector from '../components/dateRangeSelector.jsx'; +import IndividualStatsSection from '../summarySections/individualPageStatisticsSection.jsx'; +import IndividualMetricsSection from '../metricsSections/individualPageMetricsSection.jsx'; +import IndividualHistoricSection from '../historicalSections/individualPageHistoricalSection.jsx'; + +import { + transformCommitsDataForUser, + getGaugeChartDataCommits, + getGaugeChartDataModifiedLines, + transformModifiedLinesDataForUser +} from '../domain/commits.jsx' +import { + getGaugeDataAssignedIssuesPerUser, + getGaugeDataClosedIssuesPerUser, + transformAssignedIssuesDataForUser, + transformClosedIssuesDataForUser +} from '../domain/issues.jsx' +import { + getGaugeDataCreatedPRsPerUser, + getGaugeDataMergedPRsPerUser, + transformCreatedPRsDataForUser, + transformMergedPRsDataForUser +} from '../domain/pullRequests.jsx' +import { + getGaugeDataAssignedTasksPerUser, + getGaugeDataInProgressTasksPerUser, + getGaugeDataDoneTasksPerUser, + getGaugeDataStandardStatusTasksPerUser, + transformTasksAssignedDataForUser, + transformTasksToDoDataForUser, + transformTasksInProgressDataForUser, + transformTasksDoneDataForUser, + transformTasksStandardDataForUser, +} from '../domain/projects.jsx' +import { + filterHistoricData, } +from '../domain/utils.jsx'; + +function IndividualPage({ data, historicData, features }) { + const [selectedUser, setSelectedUser] = usePersistentState("selectedUser",Object.keys(data.avatars)[0]); + const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIndividual', false); + const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIndividual', "7"); + + //Statistics + const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null; + const users = Object.keys(data.avatars); + const avatar = data.avatars[selectedUser] || ""; + const commits = data.commits[selectedUser] || 0; + const modifiedLines = data.modified_lines[selectedUser] || { additions: 0, deletions: 0, modified: 0 }; + const streak = data.commit_streak[selectedUser] || 0; + const longestStreak = data.longest_commit_streak_per_user[selectedUser] || 0; + const issuesAssigned = data.issues.assigned[selectedUser] || 0; + const issuesClosed = data.issues.closed[selectedUser] || 0; + const pullRequestsCreated = data.pull_requests.created[selectedUser] + const pullRequestsMerged = data.pull_requests.merged_per_member[selectedUser] + const totalPeople = Object.keys(data.avatars).length; + const tasksAssigned = data.project.metrics_by_iteration.total.assigned_per_member[selectedUser] || 0 + const tasksClosed = data.project.metrics_by_iteration.total.done_per_member[selectedUser] || 0 + const tasksInProgress = data.project.metrics_by_iteration.total.in_progress_per_member[selectedUser] || 0 + const tasksTodo = data.project.metrics_by_iteration.total.todo_per_member[selectedUser] || 0 + const tasksStandard = tasksClosed + tasksInProgress + tasksTodo + + //Gauges + const usersCommits = getGaugeChartDataCommits(data) + const percentageCommits = usersCommits.find(userObj => userObj.user === selectedUser).percentage; + const usersModifiedLines = getGaugeChartDataModifiedLines(data) + const percentageModifiedlLines = usersModifiedLines.find(userObj => userObj.user === selectedUser).percentage; + + const usersAssignedIssues = getGaugeDataAssignedIssuesPerUser(data) + const percentageAssigned = usersAssignedIssues.find(userObj => userObj.user === selectedUser).percentage; + const usersClosedIssues = getGaugeDataClosedIssuesPerUser(data) + const percentageIssuesClosed = usersClosedIssues.find(userObj => userObj.user === selectedUser).percentage; + + const usersCreatedPRs = getGaugeDataCreatedPRsPerUser(data) + const percentageCreated = usersCreatedPRs.find(userObj => userObj.user === selectedUser).percentage; + const usersMergedPRs = getGaugeDataMergedPRsPerUser(data) + const percentageMerged = usersMergedPRs.find(userObj => userObj.user === selectedUser).percentage; + + const usersTasksAssigned = getGaugeDataAssignedTasksPerUser(data,"total"); + const percentageTasksAssigned = usersTasksAssigned.find(userObj => userObj.user === selectedUser).percentage; + const usersTasksInProgress = getGaugeDataInProgressTasksPerUser(data,"total"); + const percentageTasksInProgress = usersTasksInProgress.find(userObj => userObj.user === selectedUser).percentage; + const usersTasksDone = getGaugeDataDoneTasksPerUser(data,"total"); + const percentageTasksDone = usersTasksDone.find(userObj => userObj.user === selectedUser).percentage; + const usersTasksStandard = getGaugeDataStandardStatusTasksPerUser(data,"total"); + const percentageTasksStandard = usersTasksStandard.find(userObj => userObj.user === selectedUser).percentage; + + //Historic + const { xDataCommits, yDataCommits } = transformCommitsDataForUser(filteredhistoricaData, selectedUser) + + const { xDataModifiedLines, yDataModifiedLines } = transformModifiedLinesDataForUser(filteredhistoricaData, selectedUser) + + const { xDataAssignedIssues, yDataAssignedIssues } = transformAssignedIssuesDataForUser(filteredhistoricaData, selectedUser); + + const { xDataClosedIssues, yDataClosedIssues } = transformClosedIssuesDataForUser(filteredhistoricaData, selectedUser); + + const { xDataCreatedPRs, yDataCreatedPRs } = transformCreatedPRsDataForUser(filteredhistoricaData, selectedUser); + + const { xDataMergedPRs, yDataMergedPRs } = transformMergedPRsDataForUser(filteredhistoricaData, selectedUser); + + const { xDataTasksAssigned, yDataTasksAssigned } = transformTasksAssignedDataForUser(filteredhistoricaData, selectedUser); + + const { xDataTasksToDo, yDataTasksToDo } = transformTasksToDoDataForUser(filteredhistoricaData, selectedUser); + + const { xDataTasksInProgress, yDataTasksInProgress } = transformTasksInProgressDataForUser(filteredhistoricaData, selectedUser); + + const { xDataTasksDone, yDataTasksDone } = transformTasksDoneDataForUser(filteredhistoricaData, selectedUser); + + const { xDataTasksStandard, yDataTasksStandard } = transformTasksStandardDataForUser(filteredhistoricaData, selectedUser); + + return ( +
+

Individual overview

+ + + + + + + {!showHistorical && ( + + )} + {showHistorical && ( + + )} +
+ ); +} + +export default IndividualPage; diff --git a/src/pages/issues.jsx b/src/pages/issues.jsx deleted file mode 100644 index 10cd447..0000000 --- a/src/pages/issues.jsx +++ /dev/null @@ -1,241 +0,0 @@ -import React, { useState } from 'react'; -import GaugeChart from '../components/gaugeChart'; -import RadarPieToggle from '../components/radarPieToggle'; -import LineChartMultiple from '../components/lineChartMultiple'; -import usePersistentStateSession from '../components/usePersistentStateSession'; - -import '../styles/commits.css'; - -function Issues({ data,historicData,features }) { - const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIssues', false); - const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIssues', "7"); - const filterHistoricData = (data, days) => { - if (days === "lifetime") return data; - - const today = new Date(); - const cutoff = new Date(today); - cutoff.setDate(today.getDate() - parseInt(days)); - const cutoffDateString = cutoff.toISOString().split("T")[0]; - const filtered = {}; - for (const date in data) { - if (date >= cutoffDateString) { - filtered[date] = data[date]; - } - } - - return filtered; - }; - const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null; - - const issuesData = data.issues; - const totalIssues = issuesData.total; - const totalClosed = issuesData.total_closed - const havePullRequest = issuesData.have_pull_request - const { non_assigned, ...filteredData } = issuesData.assigned; - const totalPeople = Object.keys(data.avatars).length; - const totalAssigned = totalIssues - issuesData.assigned['non_assigned'] - const closedBy = issuesData.closed - - - const transformAssignedPRsDataForLineChart = (data) => { - const xDataAssigned = []; - const userSeries = {}; - - for (const date in data) { - xDataAssigned.push(date); - const issues = data[date].issues.assigned; - - for (const user in issues) { - if(user === 'non_assigned') continue; - if (!userSeries[user]) { - userSeries[user] = []; - } - userSeries[user].push(issues[user]); - } - } - const seriesDataAssigned = Object.keys(userSeries).map(user => ({ - name: user, - data: userSeries[user] - })); - - return { xDataAssigned, seriesDataAssigned}; - }; - - const { xDataAssigned, seriesDataAssigned } = transformAssignedPRsDataForLineChart(filteredhistoricaData); - - - return ( -
-

Issues

-
-
- - -
-
- - {showHistorical && ( -
- -
- )} - - {!showHistorical && ( - <> -
-

Summary

-
-
- -
-
-

- Issues assigned - - ⓘ - Percentage of Issues that are assigned to a user relative to the total number of Issues - -

- 0 ? totalAssigned / totalIssues : 0} - totalPeople= {1} - /> -
- {features.includes("pull-requests") && ( -
-

- Issues closed with Pull Request - - ⓘ - - Percentage of closed Issues that have a Pull Request associated - - -

- 0 ? havePullRequest / totalClosed : 0} - totalPeople= {1} - /> -
)} -
-
-
-

Metrics by User

-

- Issues assigned per user - - ⓘ - Percentage of issues assigned per user relative to the number of assigned issues - -

-
- {Object.keys(issuesData.assigned).map((user) => { - if (user !== 'non_assigned') { - const userIssues = issuesData.assigned[user]; - const percentage = totalAssigned > 0 ? userIssues / totalAssigned : 0; - return ( - - ); - } - return null; - })} -
-

- Issues closed per user - - ⓘ - - Percentage of issues closed per user relative to the issues assigned to that user - - -

-
- {Object.keys(closedBy).map((user) => { - const closedCount = closedBy[user]; - const assigned = issuesData.assigned[user]; - const percentage = assigned > 0 ? closedCount / assigned : 0; - return ( - - ); - })} -
-
- )} - -{showHistorical && ( - <> - {historicData ? ( - <> -
-
-
- -
-
-
- - ) : ( -
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop - s'hagi fet la primera execució del - workflow Daily Metrics. -
)} - - )} - -
- ); -} - -export default Issues; diff --git a/src/pages/issuesPage.jsx b/src/pages/issuesPage.jsx new file mode 100644 index 0000000..8325df5 --- /dev/null +++ b/src/pages/issuesPage.jsx @@ -0,0 +1,79 @@ +// pages/IssuesPage.jsx +import React from 'react'; +import usePersistentStateSession from '../components/usePersistentStateSession'; +import HistoricalToggle from '../components/historicalToggle.jsx'; +import DateRangeSelector from '../components/dateRangeSelector.jsx'; +import { + transformAssignedIssuesDataForLineChart, + getRadarAndPieDataIssuesAssigned, + getGaugeDataIssuesAssigned, + getGaugeDataAssignedIssuesPerUser, + getGaugeDataClosedIssuesPerUser, + getGaugeDataIssuesHavePR +} from '../domain/issues'; +import { filterHistoricData } from '../domain/utils.jsx'; + +import IssuesSummarySection from '../summarySections/issuesPageSummarySection.jsx'; +import IssuesMetricsByUserSection from '../metricsSections/issuesPageMetricsSection.jsx'; +import IssuesHistoricalSection from '../historicalSections/issuesPageHistoricalSection.jsx'; + +import '../styles/elements.css'; + +function IssuesPage({ data, historicData, features }) { + const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalIssues', false); + const [dateRange, setDateRange] = usePersistentStateSession('dateRangeIssues', "7"); + + const filteredHistoricalData = historicData ? filterHistoricData(historicData, dateRange) : null; + const totalPeople = Object.keys(data.avatars).length; + + const { xDataAssigned, seriesDataAssigned } = transformAssignedIssuesDataForLineChart(filteredHistoricalData); + const percentageAssigned = getGaugeDataIssuesAssigned(data); + const gaugeDataHavePR = getGaugeDataIssuesHavePR(data); + const gaugeDataAssigned = getGaugeDataAssignedIssuesPerUser(data); + const gaugeDataClosed = getGaugeDataClosedIssuesPerUser(data); + const { radarData, pieData } = getRadarAndPieDataIssuesAssigned(data); + + return ( +
+

Issues

+ + + + + + {!showHistorical && ( + <> + + + + )} + + {showHistorical && ( + + )} +
+ ); +} + +export default IssuesPage; diff --git a/src/pages/projects.jsx b/src/pages/projects.jsx deleted file mode 100644 index b1c65f8..0000000 --- a/src/pages/projects.jsx +++ /dev/null @@ -1,649 +0,0 @@ -import React, { useState } from 'react'; -import GaugeChart from '../components/gaugeChart'; -import RadarPieToggle from '../components/radarPieToggle'; -import PieChart from '../components/pieChart'; -import LineChartMultiple from '../components/lineChartMultiple'; -import AreaChartMultiple from '../components/areaChartMultiple'; -import usePersistentStateSession from '../components/usePersistentStateSession'; - -import '../styles/commits.css'; - -function Projects({ data,historicData,features }) { - const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalProject', false); - const [dateRange, setDateRange] = usePersistentStateSession('dateRangeProject', "7"); - const today = new Date(); - -const getActiveIteration = () => { - const has_iterations = data.project.has_iterations; - if (!has_iterations) return "total"; - const iterations = data.project.iterations - for (const key in iterations) { - const { startDate, endDate } = iterations[key]; - const start = new Date(startDate); - const end = new Date(endDate); - - if (today >= start && today <= end) { - return key; - } - } - return "total"; - }; - - const [selectedIteration, setSelectedIteration] = usePersistentStateSession("activeIteration",getActiveIteration()); - const filterHistoricData = (data, days) => { - if (days === "lifetime") return data; - - const today = new Date(); - const cutoff = new Date(today); - cutoff.setDate(today.getDate() - parseInt(days)); - const cutoffDateString = cutoff.toISOString().split("T")[0]; - const filtered = {}; - for (const date in data) { - if (date >= cutoffDateString) { - filtered[date] = data[date]; - } - } - - return filtered; - }; - const filterHistoricDataByIteration = (dataHist, iterationName) => { - if (!data) return null; - if (iterationName === "total") return dataHist; - - const iteration = data.project.iterations[iterationName] || null; - if (!iteration) return dataHist; - - const startDate = new Date(iteration.startDate); - const endDate = new Date(iteration.endDate); - - const filtered = {}; - for (const dateStr in dataHist) { - const date = new Date(dateStr); - if (date >= startDate && date <= endDate) { - filtered[dateStr] = dataHist[dateStr]; - } - } - return filtered; - }; - - const iterationsOrdered = Object.keys(data.project.iterations); - const iterations = Object.keys(data.project.metrics_by_iteration); - - const sortedIterationNames = [ - ...iterationsOrdered, - ...iterations.filter(name => name === "total"), - ]; - - const filteredhistoricaData = historicData - ? (selectedIteration === "total" - ? filterHistoricData(historicData, dateRange) - : filterHistoricDataByIteration(historicData, selectedIteration)) - : null; - const taskData = data.project.metrics_by_iteration[selectedIteration]; - const totalTasks = taskData.total_tasks || 0 - const totalInProgress = taskData.in_progress || 0 - const totalDone = taskData.done || 0 - const totalToDo = taskData.todo || 0 - - const totalStandardStatuses = totalInProgress + totalDone + totalToDo - - const { non_assigned, ...assignedPerMember } = taskData.assigned_per_member; - const totalAssigned = Object.values(assignedPerMember).reduce((sum, current) => sum + current, 0); - const inProgresPerMember = taskData.in_progress_per_member || 0; - const donePerMember = taskData.done_per_member || 0; - const todoPerMember = taskData.todo_per_member || 0; - const totalPeople = Object.keys(data.avatars).length; - - const totalIssues = taskData.total_issues - const totalIssuesWithType = taskData.total_issues_with_type - const totalItems = taskData.total - - const taskDataNoIteration = data.project.metrics_by_iteration['no_iteration'] - const taskDataTotal = data.project.metrics_by_iteration['total'] - const itemsTotal = taskDataTotal.total - const itemsNoIteration = taskDataNoIteration.total - const transformAssignedDataForLineChart = (dataHistoric, selectedIteration, iterations) => { - let allDates = []; - if (!dataHistoric) return { xDataAssigned: [], seriesDataAssigned: [] } - - if (selectedIteration === "total") { - allDates = Object.keys(dataHistoric).sort((a,b) => new Date(a) - new Date(b)); - } else { - const iteration = iterations[selectedIteration]; - if (!iteration) return { xDataAssigned: [], seriesDataAssigned: [] }; - - const startDate = new Date(iteration.startDate); - const endDate = new Date(iteration.endDate); - - const dates = []; - let currentDate = new Date(startDate); - while (currentDate <= endDate) { - const dateStr = currentDate.toISOString().split('T')[0]; - dates.push(dateStr); - currentDate.setDate(currentDate.getDate() + 1); - } - allDates = dates; - } - - const userSeries = {}; - const allUsers = new Set(); - - const assignedPerMember = data.project.metrics_by_iteration["total"]?.assigned_per_member || {}; - - Object.keys(assignedPerMember) - .filter(u => u !== "non_assigned") - .forEach(u => allUsers.add(u)); - - allUsers.forEach(u => { - userSeries[u] = []; - }); - - for (const date of allDates) { - const dayData = dataHistoric[date]; - const iterationData = dayData?.project?.metrics_by_iteration?.[selectedIteration]; - const assignedPerMember = iterationData?.assigned_per_member || {}; - allUsers.forEach(u => { - userSeries[u].push(assignedPerMember[u] || null); - }); - } - - const seriesDataAssigned = Array.from(allUsers).map(u => ({ - name: u, - data: userSeries[u], - })); - - return { xDataAssigned: allDates, seriesDataAssigned }; -}; - - const { xDataAssigned, seriesDataAssigned } = transformAssignedDataForLineChart( - filteredhistoricaData, - selectedIteration, - data.project.iterations - ); - - const transformFeatureDataForAreaChart = (data) => { - const xDataFeature = []; - const knownKeys = { - total_features_done: 'Done', - total_features_in_progress: 'In Progress', - total_features_todo: 'To Do', - }; - - const baseFeatureData = { - Done: [], - 'In Progress': [], - 'To Do': [], - }; - - const otherKeysData = {}; - const allOtherKeys = new Set(); - - for (const date in data) { - const metrics = data[date]?.project?.metrics_by_iteration?.total; - if (!metrics) continue; - - for (const [key, value] of Object.entries(metrics)) { - if ( - !Object.keys(knownKeys).includes(key) && - typeof value === 'number' && - key.startsWith('total_features_') && - Number.isInteger(value) - ) { - allOtherKeys.add(key); - } - } - } - - for (const key of allOtherKeys) { - otherKeysData[key] = []; - } - - for (const date in data) { - const metrics = data[date]?.project?.metrics_by_iteration?.total || {}; - - xDataFeature.push(date); - - for (const [key, label] of Object.entries(knownKeys)) { - baseFeatureData[label].push(metrics[key] || 0); - } - - for (const key of allOtherKeys) { - const value = metrics[key]; - otherKeysData[key].push(Number.isInteger(value) ? value : 0); - } - } - - return { - xDataFeature, - doneFeature: baseFeatureData['Done'], - inProgressFeature: baseFeatureData['In Progress'], - toDoFeature: baseFeatureData['To Do'], - otherKeysData, - }; - }; - - - const { - xDataFeature, - doneFeature, - inProgressFeature, - toDoFeature, - otherKeysData - } = transformFeatureDataForAreaChart(filteredhistoricaData); - - const baseSeries = [ - { label: 'Done', data: doneFeature, color: 'rgb(0, 255, 0)' }, - { label: 'In Progress', data: inProgressFeature, color: 'orange' }, - { label: 'To Do', data: toDoFeature, color: 'rgb(255, 0, 0)' } - ]; - - const otherSeries = Object.entries(otherKeysData).map(([key, data]) => ({ - label: key.replace('total_features_', '') - .replace(/_/g, ' ') - .replace(/\b\w/g, c => c.toUpperCase()), - data - })); - - const allSeries = [...baseSeries, ...otherSeries]; - - - function formatDate(dateStr) { - const d = new Date(dateStr); - const day = String(d.getDate()).padStart(2, '0'); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const year = d.getFullYear(); - return `${day}/${month}/${year}`; - } - function getDateRangeForIteration(iterationName) { - const iteration = data.project.iterations[iterationName]; - - if (!iteration) return ''; - return `${formatDate(iteration.startDate)} - ${formatDate(iteration.endDate)}`; - } - - function getTypeDataForChart(projectData,selectedIteration) { - - const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration]; - - if (!iterationData) return []; - - const { - total_tasks = 0, - total_bugs = 0, - total_features = 0, - } = iterationData; - - return [ - ['Tasks', total_tasks], - ['Features', total_features], - ['Bugs', total_bugs], - ]; - }; - -function getFeatureDataForChart(projectData, selectedIteration) { - const iterationData = projectData?.project?.metrics_by_iteration?.[selectedIteration]; - if (!iterationData) return { baseData: [], otherData: [] }; - - const baseData = []; - const otherData = []; - - const knownKeys = { - total_features_todo: 'To Do', - total_features_in_progress: 'In Progress', - total_features_done: 'Done', - }; - - for (const [key, label] of Object.entries(knownKeys)) { - const value = iterationData[key] || 0; - baseData.push({ label, value }); - } - - for (const [key, value] of Object.entries(iterationData)) { - if ( - key.startsWith('total_features_') && - !knownKeys.hasOwnProperty(key) - ) { - const label = key - .replace('total_features_', '') - .replace(/_/g, ' ') - .replace(/\b\w/g, c => c.toUpperCase()); - otherData.push({ label, value }); - } - } - - return { baseData, otherData }; -} - - const typePieChartData = getTypeDataForChart(data, selectedIteration); - const typeColorsPieChart = ["orange", "#0f58ff","red"]; - - const { baseData, otherData } = getFeatureDataForChart(data, selectedIteration); - - const featurePieChartData = [ - ...baseData.map(item => [item.label, item.value]), - ...otherData.map(item => [item.label, item.value]) - ]; const featureColorsPieChart = ["green", "orange","red"]; - return ( - -
-

Projects

-
-
- - -
-
- - {showHistorical && ( - <> -
- -
- {selectedIteration === "total" && ( -
- -
- )} - - )} - - - - {!showHistorical && ( - <> -
- -
- {getDateRangeForIteration(selectedIteration)} -
-
-
-

Summary

-
-
- -
-
- -
-
- -
-
-

- Tasks assigned - - ⓘ - Percentage of tasks that are assigned to a user relative to the total number of Tasks - -

- 0 ? totalAssigned / totalTasks : 0} - totalPeople= {1} - /> -
-
-

- Tasks with Standard Status - - ⓘ - Percentage of tasks that have a standard status (ToDo, In Progress, Done) to the total of tasks - -

- 0 ? totalStandardStatuses / totalTasks : 0} - totalPeople= {1} - /> -
-
-

- Issues - - ⓘ - Percentage of Items that are Issues - -

- 0 ? totalIssues / totalItems : 0} - totalPeople= {1} - /> -
-
-

- Issues with type - - ⓘ - Percentage of Issues that have a type assigned - -

- 0 ? totalIssuesWithType / totalIssues : 0} - totalPeople= {1} - /> -
- {data.project.has_iterations && ( -
-

- Items with iteration - - ⓘ - Percentage of Items that are assigned to an iteration - -

- 0 ? (itemsTotal - itemsNoIteration) / itemsTotal : 0} - totalPeople= {1} - /> -
)} -
-
-
-

Metrics by User

-

- Tasks assigned per user - - ⓘ - Percentage of tasks assigned per user relative to the number of assigned tasks - -

-
- {Object.keys(assignedPerMember).map((user) => { - if (user !== 'non_assigned') { - const userTasks = assignedPerMember[user]; - const percentage = totalAssigned > 0 ? userTasks / totalAssigned : 0; - return ( - - ); - } - return null; - })} -
-

- Tasks In Progress per user - - ⓘ - - Shows if the user is actively working on at least one task. - If the gauge is at 100%, the user has at least one task in progress. - If it's at 0%, the user currently has no tasks in progress. - - -

-
- {Object.keys(inProgresPerMember).map((user) => { - const inProgress = inProgresPerMember[user] - const percentage = inProgress > 0 ? 1 : 0; - return ( - - ); - })} -
-

- Tasks Done per user - - ⓘ - - Percentage of tasks done per user relative to the tasks assigned to that user - - -

-
- {Object.keys(donePerMember).map((user) => { - const doneCount = donePerMember[user]; - const assigned = assignedPerMember[user]; - const percentage = assigned > 0 ? doneCount / assigned : 0; - return ( - - ); - })} -
- -

- Tasks with Standard Status per user - - ⓘ - - Percentage of tasks with standard status (ToDo, In Progress, Done) per user relative to their assigned tasks - - -

-
- {Object.keys(assignedPerMember).map((user) => { - const assigned = assignedPerMember[user] || 0; - const inProgress = inProgresPerMember[user] || 0; - const doneCount = donePerMember[user] || 0; - const todoCount = todoPerMember[user] || 0; - const standardTotal = inProgress + doneCount + todoCount; - const percentage = assigned > 0 ? standardTotal / assigned : 0; - - return ( - - ); - })} -
-
- )} - - {showHistorical && ( - <> - {historicData ? ( - <> -
-
-
- -
-
- -
-
-
- ) : ( -
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop - s'hagi fet la primera execució del - workflow Daily Metrics. -
)} - - )} -
- ); -} - -export default Projects; diff --git a/src/pages/projectsPage.jsx b/src/pages/projectsPage.jsx new file mode 100644 index 0000000..b46ef98 --- /dev/null +++ b/src/pages/projectsPage.jsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import usePersistentStateSession from '../components/usePersistentStateSession'; +import HistoricalToggle from '../components/historicalToggle.jsx'; +import DateRangeSelector from '../components/dateRangeSelector.jsx'; + +import ProjectsPageSummarySection from '../summarySections/projectsPageSummarySection.jsx'; +import ProjectsPageMetricsSection from '../metricsSections/projectsPageMetricsSection.jsx'; +import ProjectsPageHistoricalSection from '../historicalSections/projectsPageHistoricalSection.jsx'; + +import { + getActiveIteration, + filterHistoricDataByIteration, + getRadarPieDataAssignedTasks, + transformAssignedTasksDataForLineChart, + transformFeatureDataForAreaChart, + getDateRangeForIteration, + getIssueTypeDataForChart, + getFeatureDataForChart, + getGaugeDataTasksAssigned, + getGaugeDataTasksStandardStatus, + getGaugeDataItemsIssues, + getGaugeDataItemIssuesWithType, + getGaugeDataItemIssuesWithIteration, + getGaugeDataAssignedTasksPerUser, + getGaugeDataInProgressTasksPerUser, + getGaugeDataDoneTasksPerUser, + getGaugeDataStandardStatusTasksPerUser +} from '../domain/projects'; +import { filterHistoricData } from '../domain/utils'; + +function ProjectsPage({ data, historicData, features }) { + const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalProject', false); + const [dateRange, setDateRange] = usePersistentStateSession('dateRangeProject', "7"); + + const [selectedIteration, setSelectedIteration] = usePersistentStateSession("activeIteration", getActiveIteration(data)); + + const iterationsOrdered = Object.keys(data.project.iterations); + const iterations = Object.keys(data.project.metrics_by_iteration); + + const sortedIterationNames = [ + ...iterationsOrdered, + ...iterations.filter(name => name === "total"), + ]; + const totalPeople = Object.keys(data.avatars).length; + + const filteredhistoricaData = historicData + ? (selectedIteration === "total" + ? filterHistoricData(historicData, dateRange) + : filterHistoricDataByIteration(data, historicData, selectedIteration)) + : null; + + const { radarDataAssigned, pieDataAssigned } = getRadarPieDataAssignedTasks(data, selectedIteration); + + const { xDataAssigned, seriesDataAssigned } = transformAssignedTasksDataForLineChart( + data, + filteredhistoricaData, + selectedIteration, + data.project.iterations + ); + + const { xDataFeature, allSeries } = transformFeatureDataForAreaChart(filteredhistoricaData, selectedIteration, data.project.iterations); + + const { typePieChartData, typeColorsPieChart } = getIssueTypeDataForChart(data, selectedIteration); + + const { featurePieChartData, featureColorsPieChart } = getFeatureDataForChart(data, selectedIteration); + + const percentageTasksAssigned = getGaugeDataTasksAssigned(data, selectedIteration); + const percentageStandardStatus = getGaugeDataTasksStandardStatus(data, selectedIteration); + const percentageItemsIssues = getGaugeDataItemsIssues(data, selectedIteration); + const percentageItemIssuesWithType = getGaugeDataItemIssuesWithType(data, selectedIteration); + const percentageIteration = getGaugeDataItemIssuesWithIteration(data); + const gaugeDataAssignedTasks = getGaugeDataAssignedTasksPerUser(data, selectedIteration); + const gaugeDataInProgressTasks = getGaugeDataInProgressTasksPerUser(data, selectedIteration); + const gaugeDataDoneTasks = getGaugeDataDoneTasksPerUser(data, selectedIteration); + const gaugeDataStandardTasks = getGaugeDataStandardStatusTasksPerUser(data, selectedIteration); + + return ( +
+

Projects

+ +
+ + {!showHistorical && ( +
+ {getDateRangeForIteration(data, selectedIteration)} +
+ )} +
+ {selectedIteration === "total" && ( + + )} + + {!showHistorical ? ( + <> + + + + ) : ( + + )} +
+ ); +} + +export default ProjectsPage; diff --git a/src/pages/pullRequests.jsx b/src/pages/pullRequests.jsx deleted file mode 100644 index 8204766..0000000 --- a/src/pages/pullRequests.jsx +++ /dev/null @@ -1,287 +0,0 @@ -import React, { useState } from 'react'; -import GaugeChart from '../components/gaugeChart'; -import '../styles/commits.css'; -import RadarPieToggle from '../components/radarPieToggle'; -import LineChartMultiple from '../components/lineChartMultiple'; -import usePersistentStateSession from '../components/usePersistentStateSession'; - -function PullRequests({ data,historicData,features }) { - const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalPR', false); - const [dateRange, setDateRange] = usePersistentStateSession('dateRangePR', "7"); - const filterHistoricData = (data, days) => { - if (days === "lifetime") return data; - - const today = new Date(); - const cutoff = new Date(today); - cutoff.setDate(today.getDate() - parseInt(days)); - const cutoffDateString = cutoff.toISOString().split("T")[0]; - const filtered = {}; - for (const date in data) { - if (date >= cutoffDateString) { - filtered[date] = data[date]; - } - } - - return filtered; - }; - const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null; - - const pullRequests = data.pull_requests - const createdby = pullRequests.created - const mergedby = pullRequests.merged_per_member - const totalMerged = pullRequests.merged - const totalMergedNotByAuthor = pullRequests.not_merged_by_author - const totalClosed = pullRequests.closed - const total = pullRequests.total - const totalPeople = Object.keys(data.avatars).length; - const merges = data.commit_merges - - const transformCreatedPRsDataForLineChart = (data) => { - const xDataCreated = []; - const userSeries = {}; - - for (const date in data) { - xDataCreated.push(date); - const created = data[date].pull_requests.created; - - for (const user in created) { - if (!userSeries[user]) { - userSeries[user] = []; - } - userSeries[user].push(created[user]); - } - } - const seriesDataCreated = Object.keys(userSeries).map(user => ({ - name: user, - data: userSeries[user] - })); - - return { xDataCreated, seriesDataCreated }; - }; - - const { xDataCreated, seriesDataCreated } = transformCreatedPRsDataForLineChart(filteredhistoricaData); - - const transformMergedPRsDataForLineChart = (data) => { - const xDataMerged = []; - const userSeries = {}; - - for (const date in data) { - xDataMerged.push(date); - const merged = data[date].pull_requests.merged_per_member; - - for (const user in merged) { - if (!userSeries[user]) { - userSeries[user] = []; - } - userSeries[user].push(merged[user]); - } - } - const seriesDataMerged = Object.keys(userSeries).map(user => ({ - name: user, - data: userSeries[user] - })); - - return { xDataMerged, seriesDataMerged }; - }; - - const { xDataMerged, seriesDataMerged } = transformMergedPRsDataForLineChart(filteredhistoricaData); - - return ( -
-

Pull requests

-
-
- - -
-
- - {showHistorical && ( -
- -
- )} - - {!showHistorical && ( - <> -
-

Summary

-
-
- -
-
- -
-
-
-
-

- Pull requests merged - - ⓘ - Percentage of pull requests that are merged relative to the total number of pull requests that are not closed - -

- 0 ? totalMerged / (total - totalClosed) : 0} - totalPeople= {1} - /> -
-
-

- Pull requests reviewed - - ⓘ - - Percentage of pull requests merged by another user that is not the author relative to the total number of merged pull requests - - -

- 0 ? totalMergedNotByAuthor / totalMerged : 0} - totalPeople={1} - /> -
-
-

- Merges from pull requests - - ⓘ - - Percentage of merges that are from pull requests - - -

- 0 ? totalMerged / merges : 0} - totalPeople={1} - /> -
-
-
-
-

Metrics by User

-

- Pull requests created per user - - ⓘ - Percentage of pull requests created per user relative to the total number of pull requets - -

-
- {Object.keys(createdby).map((user) => { - const userPRs = createdby[user]; - const percentage = total > 0 ? userPRs / total : 0; - return ( - - ); - })} -
-

- Pull requests merged per user - - ⓘ - Percentage of pull requests merged per user relative to the number of merged pull requets - -

-
- {Object.keys(mergedby).map((user) => { - const userMergedPRs = mergedby[user]; - const percentage = totalMerged > 0 ? userMergedPRs / totalMerged : 0; - return ( - - ); - })} -
-
- - )} - {showHistorical && ( - <> - {historicData ? ( - <> -
-
-
- -
-
- -
-
-
- - ) : ( -
- No s'ha trobat historic_metrics.json.
- Si és el primer dia, torna demà un cop - s'hagi fet la primera execució del - workflow Daily Metrics. -
)} - - )} -
- ); -} - -export default PullRequests; diff --git a/src/pages/pullRequestsPage.jsx b/src/pages/pullRequestsPage.jsx new file mode 100644 index 0000000..b7e90ae --- /dev/null +++ b/src/pages/pullRequestsPage.jsx @@ -0,0 +1,97 @@ +import React from 'react'; +import usePersistentStateSession from '../components/usePersistentStateSession'; +import { + transformCreatedPRsDataForLineChart, + transformMergedPRsDataForLineChart, + getRadarAndPieDataForCreatedPRs, + getRadarAndPieDataForMergedPRs, + getGaugeChartDataMergedPRs, + getGaugeChartDataReviewedPRs, + getGaugeChartDataMergesPRs, + getGaugeDataCreatedPRsPerUser, + getGaugeDataMergedPRsPerUser +} from '../domain/pullRequests'; +import HistoricalToggle from '../components/historicalToggle'; +import DateRangeSelector from '../components/dateRangeSelector.jsx'; +import { filterHistoricData } from '../domain/utils'; + +import PullRequestsPageSummarySection from '../summarySections/pullRequestsPageSummarySection.jsx'; +import PullRequestsPageMetricsSection from '../metricsSections/pullRequestsPageMetricsSection.jsx'; +import PullRequestsPageHistoricalSection from '../historicalSections/pullRequestsPageHistoricalSection.jsx'; + +import '../styles/elements.css'; + +function PullRequestsPage({ data, historicData, features }) { + const [showHistorical, setShowHistorical] = usePersistentStateSession('showHistoricalPR', false); + const [dateRange, setDateRange] = usePersistentStateSession('dateRangePR', "7"); + + const filteredhistoricaData = historicData ? filterHistoricData(historicData, dateRange) : null; + + const totalPeople = Object.keys(data.avatars).length; + + const { xDataCreated, seriesDataCreated } = transformCreatedPRsDataForLineChart(filteredhistoricaData); + const { xDataMerged, seriesDataMerged } = transformMergedPRsDataForLineChart(filteredhistoricaData); + + const { radarDataCreated, pieDataCreated } = getRadarAndPieDataForCreatedPRs(data); + const { radarDataMerged, pieDataMerged } = getRadarAndPieDataForMergedPRs(data); + + const percentageMerged = getGaugeChartDataMergedPRs(data); + const percentatgeReviewed = getGaugeChartDataReviewedPRs(data); + const percentageMergesPR = getGaugeChartDataMergesPRs(data); + + const gaugeDataCreatedPRs = getGaugeDataCreatedPRsPerUser(data); + const gaugeDataMergedPRs = getGaugeDataMergedPRsPerUser(data); + + return ( +
+

Pull requests

+ +
+ +
+ + {showHistorical && ( + + )} + + {!showHistorical && ( + <> + + + + )} + + {showHistorical && ( + + )} +
+ ); +} + +export default PullRequestsPage; diff --git a/src/styles/commits.css b/src/styles/elements.css similarity index 100% rename from src/styles/commits.css rename to src/styles/elements.css diff --git a/src/summarySections/commitsPageSummarySection.jsx b/src/summarySections/commitsPageSummarySection.jsx new file mode 100644 index 0000000..444edbb --- /dev/null +++ b/src/summarySections/commitsPageSummarySection.jsx @@ -0,0 +1,49 @@ +// components/summarySection.jsx +import React from 'react'; +import RadarPieToggle from '../components/radarPieToggle.jsx'; +import GaugeChart from '../components/gaugeChart.jsx'; + +function CommitsPageSummarySection({ radarChartCommits, radarChartModifiedLines, dataPieChartCommits, dataPieChartModifiedLines, gaugeDataAnonymous }) { + return ( +
+

Summary

+
+
+ +
+
+ +
+
+

+ Non-anonymous commits + + ⓘ + + Percentage of commits that have a member of the project as its author + + +

+ +
+
+
+ ); +} + +export default CommitsPageSummarySection; diff --git a/src/summarySections/indexStatsSection.jsx b/src/summarySections/indexStatsSection.jsx new file mode 100644 index 0000000..568c132 --- /dev/null +++ b/src/summarySections/indexStatsSection.jsx @@ -0,0 +1,35 @@ +import React from 'react'; + +function IndexStatsSection({ data, features }) { + return ( +
+
+
+

Project Statistics

+
+
+
Total Members: {Object.keys(data.avatars).length}
+
Total Commits: {data.commits.total}
+
Total Additions: {data.modified_lines.total.additions}
+
Total Deletions: {data.modified_lines.total.deletions}
+
Total Modifications: {data.modified_lines.total.modified}
+
Total Lines of code: {data.modified_lines.total.additions - data.modified_lines.total.deletions}
+ {features.includes("issues") && ( +
Total Issues: {data.issues.total}
+ )} + {features.includes("pull-requests") && ( +
Total Pull Requests: {data.pull_requests.total}
+ )} + {features.includes("projects") && ( + <> +
Total Tasks: {data.project.metrics_by_iteration.total.total_tasks}
+
Total Features: {data.project.metrics_by_iteration.total.total_features}
+ + )} +
+
+
+ ); +} + +export default IndexStatsSection; diff --git a/src/summarySections/individualPageStatisticsSection.jsx b/src/summarySections/individualPageStatisticsSection.jsx new file mode 100644 index 0000000..d8525a6 --- /dev/null +++ b/src/summarySections/individualPageStatisticsSection.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { truncateName } from '../domain/utils'; + +function IndividualStatsSection({ + selectedUser, + setSelectedUser, + users, + avatar, + commits, + modifiedLines, + streak, + longestStreak, + issuesAssigned, + issuesClosed, + pullRequestsCreated, + pullRequestsMerged, + tasksAssigned, + tasksTodo, + tasksInProgress, + tasksClosed, + tasksStandard, + features +}) { + return (
+
+
+ {selectedUser} + +
+
+
Commits: {commits}
+
Additions: {modifiedLines.additions}
+
Deletions: {modifiedLines.deletions}
+
Modifications: {modifiedLines.modified}
+
Commit Streak: {streak}
+
Longest Streak: {longestStreak}
+ {features.includes("issues") && ( +
Issues Assigned: {issuesAssigned}
)} + {features.includes("issues") && ( +
Issues Closed: {issuesClosed}
)} + {features.includes("pull-requests") && ( +
Pull Requests Created: {pullRequestsCreated}
)} + {features.includes("pull-requests") && ( +
Pull Requests merged: {pullRequestsMerged}
)} + {features.includes("projects") && ( +
Tasks assigned: {tasksAssigned}
)} + {features.includes("projects") && ( +
Tasks ToDo: {tasksTodo}
)} + {features.includes("projects") && ( +
Tasks In Progress: {tasksInProgress}
)} + {features.includes("projects") && ( +
Tasks Done: {tasksClosed}
)} + {features.includes("projects") && ( +
Tasks with a standard status: {tasksStandard}
)} +
+
+
+ ); +} + +export default IndividualStatsSection; diff --git a/src/summarySections/issuesPageSummarySection.jsx b/src/summarySections/issuesPageSummarySection.jsx new file mode 100644 index 0000000..f5eab44 --- /dev/null +++ b/src/summarySections/issuesPageSummarySection.jsx @@ -0,0 +1,62 @@ +// components/issuesSummarySection.jsx +import React from 'react'; +import GaugeChart from '../components/gaugeChart.jsx'; +import RadarPieToggle from '../components/radarPieToggle.jsx'; + +function IssuesPageSummarySection({ radarData, pieData, percentageAssigned, gaugeDataHavePR, features }) { + return ( +
+

Summary

+
+
+ +
+ +
+

+ Issues assigned + + ⓘ + + Percentage of Issues that are assigned to a user relative to the total number of Issues + + +

+ +
+ + {features.includes("pull-requests") && ( +
+

+ Issues closed with Pull Request + + ⓘ + + Percentage of closed Issues that have a Pull Request associated + + +

+ +
+ )} +
+
+ ); +} + +export default IssuesPageSummarySection; diff --git a/src/summarySections/projectsPageSummarySection.jsx b/src/summarySections/projectsPageSummarySection.jsx new file mode 100644 index 0000000..1593f4c --- /dev/null +++ b/src/summarySections/projectsPageSummarySection.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import RadarPieToggle from '../components/radarPieToggle'; +import PieChart from '../components/pieChart'; +import GaugeChart from '../components/gaugeChart'; + +function ProjectsPageSummarySection({ + radarDataAssigned, + pieDataAssigned, + typePieChartData, + typeColorsPieChart, + featurePieChartData, + featureColorsPieChart, + percentageTasksAssigned, + percentageStandardStatus, + percentageItemsIssues, + percentageItemIssuesWithType, + percentageIteration, + data, +}) { + return ( +
+

Summary

+
+
+ +
+
+ +
+
+ +
+
+

+ Tasks assigned + + ⓘ + + Percentage of tasks that are assigned to a user relative to the total number of Tasks + + +

+ +
+
+

+ Tasks with standard status + + ⓘ + + Percentage of tasks that have a standard status (ToDo, In Progress, Done) to the total of tasks + + +

+ +
+
+

+ Issues + + ⓘ + Percentage of Items that are Issues + +

+ +
+
+

+ Issues with type + + ⓘ + Percentage of Issues that have a type assigned + +

+ +
+ {data.project.has_iterations && ( +
+

+ Items with iteration + + ⓘ + Percentage of Items that are assigned to an iteration + +

+ +
+ )} +
+
+ ); +} + +export default ProjectsPageSummarySection; \ No newline at end of file diff --git a/src/summarySections/pullRequestsPageSummarySection.jsx b/src/summarySections/pullRequestsPageSummarySection.jsx new file mode 100644 index 0000000..94e1e13 --- /dev/null +++ b/src/summarySections/pullRequestsPageSummarySection.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import GaugeChart from '../components/gaugeChart.jsx'; +import RadarPieToggle from '../components/radarPieToggle.jsx'; + +function PullRequestsPageSummarySection({ + radarDataCreated, + pieDataCreated, + radarDataMerged, + pieDataMerged, + percentageMerged, + percentatgeReviewed, + percentageMergesPR, + features +}) { + return ( +
+

Summary

+
+
+ +
+
+ +
+
+
+
+

+ Pull requests merged + + ⓘ + + Percentage of pull requests that are merged relative to the total number of pull requests that are not closed + + +

+ +
+
+

+ Pull requests reviewed + + ⓘ + + Percentage of pull requests merged by another user that is not the author relative to the total number of merged pull requests + + +

+ +
+
+

+ Merges from pull requests + + ⓘ + + Percentage of merges that are from pull requests + + +

+ +
+
+
+ ); +} + +export default PullRequestsPageSummarySection; diff --git a/src/tests/commits.test.jsx b/src/tests/commits.test.jsx index 1755791..1c64a31 100644 --- a/src/tests/commits.test.jsx +++ b/src/tests/commits.test.jsx @@ -1,107 +1,179 @@ import { - filterHistoricData, + getGaugeDataAnonymous, transformCommitsDataForLineChart, transformModifiedLinesDataForLineChart, - prepareRadarData, - getPieChartData, - getGaugeChartPercentages, - getGaugeChartModifiedLines, + GetRadarDataCommits, + GetRadarDataModifiedLines, + getPieChartDataCommits, + getPieChartDataModifiedLines, + getGaugeChartDataCommits, + getGaugeChartDataModifiedLines, + transformCommitsDataForUser, + transformModifiedLinesDataForUser, + transformDataForLineChartCommits, + transformDataForLineChartModifiedLines } from '../domain/commits'; -describe('commitsCalculations utils', () => { +describe('commits ', () => { const mockHistoricData = { '2025-06-01': { commits: { pau: 3, lluis: 5, total: 8, - anonymous: 1 + anonymous: 1, }, modified_lines: { pau: { modified: 100 }, lluis: { modified: 200 }, - total: { modified: 300 } - } + total: { modified: 300 }, + }, }, '2025-06-02': { commits: { pau: 4, lluis: 6, total: 10, - anonymous: 2 + anonymous: 2, }, modified_lines: { pau: { modified: 150 }, lluis: { modified: 250 }, - total: { modified: 400 } - } - } + total: { modified: 400 }, + }, + }, }; - - test('filterHistoricData returns full data for "lifetime"', () => { - expect(filterHistoricData(mockHistoricData, 'lifetime')).toEqual(mockHistoricData); - }); - test('transformCommitsDataForLineChart works as expected', () => { - const { xDataCommits, seriesDataCommits } = transformCommitsDataForLineChart(mockHistoricData); - expect(xDataCommits).toEqual(['2025-06-01', '2025-06-02']); - expect(seriesDataCommits).toEqual([ + const { xData, seriesData } = transformCommitsDataForLineChart(mockHistoricData); + expect(xData).toEqual(['2025-06-01', '2025-06-02']); + expect(seriesData).toEqual([ { name: 'pau', data: [3, 4] }, - { name: 'lluis', data: [5, 6] } + { name: 'lluis', data: [5, 6] }, ]); }); test('transformModifiedLinesDataForLineChart works as expected', () => { - const { xDataModified, seriesModified } = transformModifiedLinesDataForLineChart(mockHistoricData); - expect(xDataModified).toEqual(['2025-06-01', '2025-06-02']); - expect(seriesModified).toEqual([ + const { xData, seriesData } = transformModifiedLinesDataForLineChart(mockHistoricData); + expect(xData).toEqual(['2025-06-01', '2025-06-02']); + expect(seriesData).toEqual([ { name: 'pau', data: [100, 150] }, - { name: 'lluis', data: [200, 250] } + { name: 'lluis', data: [200, 250] }, ]); }); - test('prepareRadarData returns correct structure', () => { + test('GetRadarDataCommits returns correct radar data excluding total and anonymous', () => { + const input = { + commits: { + pau: 7, + lluis: 8, + total: 15, + anonymous: 3, + }, + }; + expect(GetRadarDataCommits(input)).toEqual({ pau: 7, lluis: 8 }); + }); + + test('GetRadarDataModifiedLines returns correct radar data excluding total', () => { + const input = { + modified_lines: { + pau: { modified: 100 }, + lluis: { modified: 200 }, + total: { modified: 300 }, + }, + }; + expect(GetRadarDataModifiedLines(input)).toEqual({ pau: 100, lluis: 200 }); + }); + + test('getPieChartDataCommits returns filtered array excluding total and anonymous', () => { const input = { - pau: { modified: 100 }, - lluis: { modified: 200 } + commits: { + pau: 5, + lluis: 10, + total: 15, + anonymous: 2, + }, }; - expect(prepareRadarData(input)).toEqual({ pau: 100, lluis: 200 }); + expect(getPieChartDataCommits(input)).toEqual([ + ['pau', 5], + ['lluis', 10], + ]); }); - test('getPieChartData with key works', () => { + test('getPieChartDataModifiedLines returns filtered array excluding total', () => { const input = { - pau: { modified: 100 }, - lluis: { modified: 200 }, - total: { modified: 300 } + modified_lines: { + pau: { modified: 100 }, + lluis: { modified: 200 }, + total: { modified: 300 }, + }, }; - expect(getPieChartData(input, 'modified')).toEqual([ + expect(getPieChartDataModifiedLines(input)).toEqual([ ['pau', 100], - ['lluis', 200] + ['lluis', 200], ]); }); - test('getGaugeChartPercentages works', () => { + test('getGaugeChartDataCommits returns correct percentages excluding total and anonymous', () => { const input = { - pau: 3, - lluis: 5, - total: 8, - anonymous: 2 + commits: { + pau: 4, + lluis: 6, + total: 10, + anonymous: 1, + }, }; - expect(getGaugeChartPercentages(input, 'total', 'anonymous')).toEqual([ - { user: 'pau', percentage: 3 / 6 }, - { user: 'lluis', percentage: 5 / 6 } + const result = getGaugeChartDataCommits(input); + expect(result).toEqual([ + { user: 'pau', percentage: 4 / 9 }, + { user: 'lluis', percentage: 6 / 9 }, ]); }); - test('getGaugeChartModifiedLines works', () => { + test('getGaugeChartDataModifiedLines returns correct percentages excluding total', () => { const input = { - pau: { modified: 100 }, - lluis: { modified: 200 }, - total: { modified: 300 } + modified_lines: { + pau: { modified: 100 }, + lluis: { modified: 300 }, + total: { modified: 400 }, + }, }; - expect(getGaugeChartModifiedLines(input, 'total')).toEqual([ - { user: 'pau', percentage: 100 / 300 }, - { user: 'lluis', percentage: 200 / 300 } + const result = getGaugeChartDataModifiedLines(input); + expect(result).toEqual([ + { user: 'pau', percentage: 100 / 400 }, + { user: 'lluis', percentage: 300 / 400 }, ]); }); + + test('transformCommitsDataForUser returns correct data for given user', () => { + const { xDataCommits, yDataCommits } = transformCommitsDataForUser(mockHistoricData, 'pau'); + expect(xDataCommits).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataCommits).toEqual([3, 4]); + }); + + test('transformModifiedLinesDataForUser returns correct data for given user', () => { + const { xDataModifiedLines, yDataModifiedLines } = transformModifiedLinesDataForUser(mockHistoricData, 'lluis'); + expect(xDataModifiedLines).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataModifiedLines).toEqual([200, 250]); + }); + + test('transformDataForLineChartCommits returns correct data', () => { + const { xData, yData } = transformDataForLineChartCommits(mockHistoricData); + expect(xData).toEqual(['2025-06-01', '2025-06-02']); + expect(yData).toEqual([8, 10]); + }); + + test('transformDataForLineChartModifiedLines returns correct data', () => { + const { xDataModifedLines, yDataModifedLines } = transformDataForLineChartModifiedLines(mockHistoricData); + expect(xDataModifedLines).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataModifedLines).toEqual([300, 400]); + }); + test('getGaugeDataAnonymous returns correct ratio of non-anonymous to total', () => { + const input = { commits: { total: 10, anonymous: 2 } }; + expect(getGaugeDataAnonymous(input)).toBe(0.8); + }); + + test('getGaugeDataAnonymous returns 0 when total is 0', () => { + const input = { commits: { total: 0, anonymous: 0 } }; + expect(getGaugeDataAnonymous(input)).toBe(0); + }); }); diff --git a/src/tests/issues.test.jsx b/src/tests/issues.test.jsx new file mode 100644 index 0000000..3e95462 --- /dev/null +++ b/src/tests/issues.test.jsx @@ -0,0 +1,118 @@ +import { + transformAssignedIssuesDataForLineChart, + getRadarAndPieDataIssuesAssigned, + getGaugeDataIssuesAssigned, + getGaugeDataAssignedIssuesPerUser, + getGaugeDataClosedIssuesPerUser, + getGaugeDataIssuesHavePR, + transformAssignedIssuesDataForUser, + transformClosedIssuesDataForUser, + transformIssuesDataForAreaChart, + getPieDataIssuesStatus +} from '../domain/issues'; + +describe('issues', () => { + const mockHistoricData = { + '2025-06-01': { + issues: { + assigned: { pau: 3, lluis: 5, non_assigned: 1 }, + closed: { pau: 2, lluis: 3, non_assigned: 1 }, + total_closed: 5, + total: 9 + }, + }, + '2025-06-02': { + issues: { + assigned: { pau: 4, lluis: 6, non_assigned: 2 }, + closed: { pau: 2, lluis: 3}, + total_closed: 5, + total: 12 + + }, + }, + }; + + const mockData = { + issues: { + total: 20, + total_closed: 10, + have_pull_request: 7, + assigned: { + pau: 5, + lluis: 5, + non_assigned: 10, + }, + closed: { + pau: 3, + lluis: 2, + }, + }, + }; + + test('transformAssignedIssuesDataForLineChart returns expected structure', () => { + expect(transformAssignedIssuesDataForLineChart(mockHistoricData)).toEqual({ + xDataAssigned: ['2025-06-01', '2025-06-02'], + seriesDataAssigned: [ + { name: 'pau', data: [3, 4] }, + { name: 'lluis', data: [5, 6] }, + ], + }); + }); + + test('getRadarAndPieDataAssigned returns radarData and pieData excluding non_assigned and total', () => { + expect(getRadarAndPieDataIssuesAssigned(mockData)).toEqual({ + radarData: { pau: 5, lluis: 5 }, + pieData: [['pau', 5], ['lluis', 5]], + }); + }); + + test('getGaugeDataAssigned returns correct overall assigned percentage', () => { + expect(getGaugeDataIssuesAssigned(mockData)).toEqual((20 - 10) / 20); + }); + + test('getGaugeDataAssignedPerUser returns array with user and percentage', () => { + expect(getGaugeDataAssignedIssuesPerUser(mockData)).toEqual([ + { user: 'pau', percentage: 5 / 10 }, + { user: 'lluis', percentage: 5 / 10 }, + ]); + }); + + test('getGaugeDataClosedPerUser returns array with user and percentage', () => { + expect(getGaugeDataClosedIssuesPerUser(mockData)).toEqual([ + { user: 'pau', percentage: 3 / 5 }, + { user: 'lluis', percentage: 2 / 5 }, + ]); + }); + + test('getGaugeDataIssuesHavePR returns correct percentage of PRs', () => { + expect(getGaugeDataIssuesHavePR(mockData)).toEqual(7 / 10); + }); + test('transformAssignedIssuesDataForUser returns correct data for given user', () => { + const { xDataAssignedIssues, yDataAssignedIssues } = transformAssignedIssuesDataForUser(mockHistoricData, 'pau'); + expect(xDataAssignedIssues).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataAssignedIssues).toEqual([3, 4]); + }); + + test('transformClosedIssuesDataForUser returns correct data for given user', () => { + const { xDataClosedIssues, yDataClosedIssues } = transformClosedIssuesDataForUser(mockHistoricData, 'pau'); + expect(xDataClosedIssues).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataClosedIssues).toEqual([2, 2]); + }); + + test('transformIssuesDataForAreaChart transforms correctly', () => { + const result = transformIssuesDataForAreaChart(mockHistoricData); + expect(result.xDataIssues).toEqual(['2025-06-01', '2025-06-02']); + expect(result.closedIssues).toEqual([5,5]); + expect(result.openIssues).toEqual([4,7]); + + }); + test('getPieDataIssuesStatus returns correct open and closed issues data and colors', () => { + expect(getPieDataIssuesStatus(mockData)).toEqual({ + pieDataIssuesStatus: [ + ['Open', 10], + ['Closed', 10], + ], + pieDataIssuesStatusColor: ['red', 'green'], + }); +}); +}); diff --git a/src/tests/projects.test.jsx b/src/tests/projects.test.jsx new file mode 100644 index 0000000..338f3b7 --- /dev/null +++ b/src/tests/projects.test.jsx @@ -0,0 +1,300 @@ +import { + getActiveIteration, + filterHistoricDataByIteration, + transformAssignedTasksDataForLineChart, + transformFeatureDataForAreaChart, + getRadarPieDataAssignedTasks, + getDateRangeForIteration, + getIssueTypeDataForChart, + getFeatureDataForChart, + getGaugeDataTasksAssigned, + getGaugeDataTasksStandardStatus, + getGaugeDataItemsIssues, + getGaugeDataItemIssuesWithType, + getGaugeDataItemIssuesWithIteration, + getGaugeDataAssignedTasksPerUser, + getGaugeDataInProgressTasksPerUser, + getGaugeDataDoneTasksPerUser, + getGaugeDataStandardStatusTasksPerUser, + transformTasksAssignedDataForUser, + transformTasksToDoDataForUser, + transformTasksInProgressDataForUser, + transformTasksDoneDataForUser, + transformTasksStandardDataForUser, + transformTaskDataForAreaChart, + getPieDataTasksStatus +} from '../domain/projects'; + +describe('projects', () => { + const mockData = { + project: { + has_iterations: true, + iterations: { + it1: { + startDate: '2025-06-01', + endDate: '2025-06-03', + }, + }, + metrics_by_iteration: { + it1: { + assigned_per_member: { + pau: 3, + lluis: 2, + non_assigned: 6, + }, + in_progress_per_member: { + pau: 1, + lluis: 0, + }, + done_per_member: { + pau: 2, + lluis: 1, + }, + todo_per_member: { + pau: 0, + lluis: 1, + }, + new_state_per_member: { + pau: 0, + lluis: 1, + }, + in_progress: 3, + done: 4, + todo: 3, + new_state: 1, + total_tasks: 11, + total_issues: 4, + total_issues_with_type: 3, + total: 12, + total_features_todo: 1, + total_features_in_progress: 2, + total_features_done: 3, + total_features_new_state: 5, + total_bugs: 2, + total_features: 3 + }, + no_iteration: { + total: 1 + }, + total: { + assigned_per_member: { + pau: 3, + lluis: 2, + non_assigned: 6, + }, + in_progress_per_member: { + pau: 1, + lluis: 0, + }, + done_per_member: { + pau: 2, + lluis: 1, + }, + todo_per_member: { + pau: 0, + lluis: 1, + }, + new_state_per_member: { + pau: 0, + lluis: 1, + }, + in_progress: 3, + done: 4, + todo: 3, + new_state: 1, + total_tasks: 12, + total_issues: 4, + total_issues_with_type: 3, + total: 13, + total_features_todo: 1, + total_features_in_progress: 2, + total_features_done: 3, + total_features_new_state: 5, + total_bugs: 2, + total_features: 11 } + } + } + }; + + const mockHistoricData = { + '2025-06-01': mockData, + '2025-06-02': mockData, + }; + +test('getActiveIteration returns correct iteration with mocked date', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-02')); + + const result = getActiveIteration(mockData); + expect(result).toBe('it1'); + + vi.useRealTimers(); +}); + + test('filterHistoricDataByIteration filters data correctly', () => { + const result = filterHistoricDataByIteration(mockData, mockHistoricData, 'it1'); + expect(Object.keys(result)).toEqual(['2025-06-01', '2025-06-02']); + }); + + test('getRadarPieDataAssignedTasks returns radar and pie data', () => { + const result = getRadarPieDataAssignedTasks(mockData, 'it1'); + expect(result).toEqual({ + radarDataAssigned: { pau: 3, lluis: 2 }, + pieDataAssigned: [['pau', 3], ['lluis', 2]], + }); + }); + + test('getGaugeDataTasksAssigned returns correct percentage', () => { + expect(getGaugeDataTasksAssigned(mockData, 'it1')).toBe((5 / 11)); + }); + + test('getGaugeDataTasksStandardStatus returns correct percentage', () => { + expect(getGaugeDataTasksStandardStatus(mockData, 'it1')).toBe((10 / 11)); + }); + + test('getGaugeDataItemsIssues returns correct percentage', () => { + expect(getGaugeDataItemsIssues(mockData, 'it1')).toBe(4 / 12); + }); + + test('getGaugeDataItemIssuesWithType returns correct percentage', () => { + expect(getGaugeDataItemIssuesWithType(mockData, 'it1')).toBe(3 / 4); + }); + + test('getGaugeDataItemIssuesWithIteration returns correct percentage', () => { + expect(getGaugeDataItemIssuesWithIteration(mockData)).toBe((13 - 1) / 13); + }); + + test('getGaugeDataAssignedTasksPerUser returns per-user assignment percentages', () => { + expect(getGaugeDataAssignedTasksPerUser(mockData, 'it1')).toEqual([ + { user: 'pau', percentage: 3 / 5 }, + { user: 'lluis', percentage: 2 / 5 }, + ]); + }); + + test('getGaugeDataInProgressTasksPerUser returns 1 if in progress > 0', () => { + expect(getGaugeDataInProgressTasksPerUser(mockData, 'it1')).toEqual([ + { user: 'pau', percentage: 1 }, + { user: 'lluis', percentage: 0 }, + ]); + }); + + test('getGaugeDataDoneTasksPerUser returns done / assigned per user', () => { + expect(getGaugeDataDoneTasksPerUser(mockData, 'it1')).toEqual([ + { user: 'pau', percentage: 2 / 3 }, + { user: 'lluis', percentage: 1 / 2 }, + ]); + }); + + test('getGaugeDataStandardStatusTasksPerUser returns correct per user', () => { + expect(getGaugeDataStandardStatusTasksPerUser(mockData, 'it1')).toEqual([ + { user: 'pau', percentage: 1 }, + { user: 'lluis', percentage: 1 }, + ]); + }); + + test('getDateRangeForIteration returns formatted date range', () => { + expect(getDateRangeForIteration(mockData, 'it1')).toBe('01/06/2025 - 03/06/2025'); + }); + + test('getIssueTypeDataForChart returns pie data and colors', () => { + const { typePieChartData, typeColorsPieChart } = getIssueTypeDataForChart(mockData, 'it1'); + expect(typePieChartData).toEqual([ + ['Tasks', 11], + ['Features', 3], + ['Bugs', 2], + ]); + expect(typeColorsPieChart).toEqual(['orange', '#0f58ff', 'red']); + }); + + test('getFeatureDataForChart returns correct pie data and colors', () => { + const { featurePieChartData, featureColorsPieChart } = getFeatureDataForChart(mockData, 'it1'); + expect(featurePieChartData).toEqual([ + ['To Do', 1], + ['In Progress', 2], + ['Done', 3], + ['New State', 5], + ]); + expect(featureColorsPieChart).toEqual(['green', 'orange', 'red']); + }); + + test('transformFeatureDataForAreaChart transforms correctly with selectedIteration "total"', () => { + const result = transformFeatureDataForAreaChart(mockHistoricData, 'total', {}); + expect(result.xDataFeature).toEqual(['2025-06-01', '2025-06-02']); + + const doneSeries = result.allSeries.find(s => s.label === 'Done'); + const inProgressSeries = result.allSeries.find(s => s.label === 'In Progress'); + const toDoSeries = result.allSeries.find(s => s.label === 'To Do'); + const customSeries = result.allSeries.find(s => s.label === 'New State'); + + expect(doneSeries.data).toEqual([3, 3]); + expect(inProgressSeries.data).toEqual([2, 2]); + expect(toDoSeries.data).toEqual([1, 1]); + expect(customSeries.data).toEqual([5, 5]); + }); + + test('transformTasksAssignedDataForUser returns correct assigned tasks data for pau', () => { + const { xDataTasksAssigned, yDataTasksAssigned } = transformTasksAssignedDataForUser(mockHistoricData, 'pau'); + expect(xDataTasksAssigned).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataTasksAssigned).toEqual([3, 3]); + }); + + test('transformTasksToDoDataForUser returns correct todo tasks data for lluis', () => { + const { xDataTasksToDo, yDataTasksToDo } = transformTasksToDoDataForUser(mockHistoricData, 'lluis'); + expect(xDataTasksToDo).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataTasksToDo).toEqual([1, 1]); + }); + + test('transformTasksInProgressDataForUser returns correct in-progress tasks data for pau', () => { + const { xDataTasksInProgress, yDataTasksInProgress } = transformTasksInProgressDataForUser(mockHistoricData, 'pau'); + expect(xDataTasksInProgress).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataTasksInProgress).toEqual([1, 1]); + }); + + test('transformTasksDoneDataForUser returns correct done tasks data for lluis', () => { + const { xDataTasksDone, yDataTasksDone } = transformTasksDoneDataForUser(mockHistoricData, 'lluis'); + expect(xDataTasksDone).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataTasksDone).toEqual([1, 1]); + }); + + test('transformTasksStandardDataForUser returns sum todo + in_progress + done for pau', () => { + const { xDataTasksStandard, yDataTasksStandard } = transformTasksStandardDataForUser(mockHistoricData, 'pau'); + expect(xDataTasksStandard).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataTasksStandard).toEqual([ + 0 + 1 + 2, + 0 + 1 + 2, + ]); + }); + + test('transformTaskDataForAreaChart estructura correcta con métricas adicionales', () => { + const result = transformTaskDataForAreaChart(mockHistoricData); + + expect(result.xDataTask).toEqual(['2025-06-01', '2025-06-02']); + + const doneSeries = result.allSeries.find(s => s.label === 'Done'); + const inProgressSeries = result.allSeries.find(s => s.label === 'In Progress'); + const toDoSeries = result.allSeries.find(s => s.label === 'To Do'); + const newStateSeries = result.allSeries.find(s => s.label === 'New State'); + + expect(doneSeries?.data).toEqual([4, 4]); + expect(inProgressSeries?.data).toEqual([3, 3]); + expect(toDoSeries?.data).toEqual([3, 3]); + expect(newStateSeries?.data).toEqual([1, 1]); + }); + + test('getPieDataTasksStatus genera datos correctos de pastel con métricas adicionales', () => { + const pie = getPieDataTasksStatus(mockData); + + expect(pie.pieDataTasksStatus).toEqual(expect.arrayContaining([ + ['Todo', 3], + ['In Progress', 3], + ['Done', 4], + ['New State', 1], + ])); + + expect(pie.pieDataTasksStatusColor).toEqual(expect.arrayContaining([ + 'red', + 'orange', + 'green', + ])); + }); +}); diff --git a/src/tests/pullRequests.test.jsx b/src/tests/pullRequests.test.jsx new file mode 100644 index 0000000..8c8c005 --- /dev/null +++ b/src/tests/pullRequests.test.jsx @@ -0,0 +1,162 @@ +import { + transformCreatedPRsDataForLineChart, + transformMergedPRsDataForLineChart, + getRadarAndPieDataForCreatedPRs, + getRadarAndPieDataForMergedPRs, + getGaugeChartDataMergedPRs, + getGaugeChartDataReviewedPRs, + getGaugeChartDataMergesPRs, + getGaugeDataCreatedPRsPerUser, + getGaugeDataMergedPRsPerUser, + transformCreatedPRsDataForUser, + transformMergedPRsDataForUser, + transformPRDataForAreaChart, + getPieDataPullRequestStatus +} from '../domain/pullRequests'; + +describe('pullRequests', () => { + const mockPRsData = { + '2025-06-01': { + pull_requests: { + created: { + pau: 2, + lluis: 3, + }, + merged_per_member: { + pau: 1, + lluis: 2, + }, + total: 5, + merged: 3, + closed: 1, + }, + }, + '2025-06-02': { + pull_requests: { + created: { + pau: 3, + lluis: 4, + }, + merged_per_member: { + pau: 2, + lluis: 3, + }, + total: 7, + merged: 5, + closed: 1, + }, + }, + }; + + test('transformCreatedPRsDataForLineChart works as expected', () => { + const { xDataCreated, seriesDataCreated } = transformCreatedPRsDataForLineChart(mockPRsData); + expect(xDataCreated).toEqual(['2025-06-01', '2025-06-02']); + expect(seriesDataCreated).toEqual([ + { name: 'pau', data: [2, 3] }, + { name: 'lluis', data: [3, 4] }, + ]); + }); + + test('transformMergedPRsDataForLineChart works as expected', () => { + const { xDataMerged, seriesDataMerged } = transformMergedPRsDataForLineChart(mockPRsData); + expect(xDataMerged).toEqual(['2025-06-01', '2025-06-02']); + expect(seriesDataMerged).toEqual([ + { name: 'pau', data: [1, 2] }, + { name: 'lluis', data: [2, 3] }, + ]); + }); + + const aggregatedData = { + pull_requests: { + created: { + pau: 3, + lluis: 5, + }, + merged_per_member: { + pau: 2, + lluis: 3, + }, + total: 10, + merged: 5, + closed: 2, + not_merged_by_author: 3, + }, + commit_merges: 6, + }; + + test('getRadarAndPieDataForCreatedPRs returns correct radar and pie data', () => { + const { radarDataCreated, pieDataCreated } = getRadarAndPieDataForCreatedPRs(aggregatedData); + expect(radarDataCreated).toEqual({ pau: 3, lluis: 5 }); + expect(pieDataCreated).toEqual([['pau', 3], ['lluis', 5]]); + }); + + test('getRadarAndPieDataForMergedPRs returns correct radar and pie data', () => { + const { radarDataMerged, pieDataMerged } = getRadarAndPieDataForMergedPRs(aggregatedData); + expect(radarDataMerged).toEqual({ pau: 2, lluis: 3 }); + expect(pieDataMerged).toEqual([['pau', 2], ['lluis', 3]]); + }); + + test('getGaugeChartDataMergedPRs returns correct percentage', () => { + const result = getGaugeChartDataMergedPRs(aggregatedData); + expect(result).toBe(5 / (10 - 2)); + }); + + test('getGaugeChartDataReviewedPRs returns correct percentage', () => { + const result = getGaugeChartDataReviewedPRs(aggregatedData); + expect(result).toBe(3 / 5); + }); + + test('getGaugeChartDataMergesPRs returns correct percentage', () => { + const result = getGaugeChartDataMergesPRs(aggregatedData); + expect(result).toBe(5 / 6); + }); + + test('getGaugeDataCreatedPRsPerUser returns correct user percentages', () => { + const result = getGaugeDataCreatedPRsPerUser(aggregatedData); + expect(result).toEqual([ + { user: 'pau', percentage: 3 / 10 }, + { user: 'lluis', percentage: 5 / 10 }, + ]); + }); + + test('getGaugeDataMergedPRsPerUser returns correct user percentages', () => { + const result = getGaugeDataMergedPRsPerUser(aggregatedData); + expect(result).toEqual([ + { user: 'pau', percentage: 2 / 5 }, + { user: 'lluis', percentage: 3 / 5 }, + ]); + }); + test('transformCreatedPRsDataForUser returns correct created PRs data for pau', () => { + const { xDataCreatedPRs, yDataCreatedPRs } = transformCreatedPRsDataForUser(mockPRsData, 'pau'); + expect(xDataCreatedPRs).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataCreatedPRs).toEqual([2, 3]); + }); + + test('transformMergedPRsDataForUser returns correct merged PRs data for lluis', () => { + const { xDataMergedPRs, yDataMergedPRs } = transformMergedPRsDataForUser(mockPRsData, 'lluis'); + expect(xDataMergedPRs).toEqual(['2025-06-01', '2025-06-02']); + expect(yDataMergedPRs).toEqual([2, 3]); + }); + + test('transformPRDataForAreaChart works as expected', () => { + const { xDataPRs, areaPRData } = transformPRDataForAreaChart(mockPRsData); + + expect(xDataPRs).toEqual(['2025-06-01', '2025-06-02']); + expect(areaPRData).toEqual([ + { label: 'Merged', data: [3, 5], color: 'rgb(0, 255, 0)' }, + { label: 'Closed', data: [1, 1], color: 'orange' }, + { label: 'Open', data: [1, 1], color: 'rgb(255, 0, 0)' }, + ]); +}); + +test('getPieDataPullRequestStatus returns correct pie data and colors', () => { + const { pieDataPullRequestStatus, pieDataPullRequestStatusColor } = getPieDataPullRequestStatus(aggregatedData); + + expect(pieDataPullRequestStatus).toEqual([ + ['Open', aggregatedData.pull_requests.total - aggregatedData.pull_requests.merged - aggregatedData.pull_requests.closed], + ['Closed', aggregatedData.pull_requests.closed], + ['Merged', aggregatedData.pull_requests.merged], + ]); + expect(pieDataPullRequestStatusColor).toEqual(['red', 'orange', 'green']); +}); +}); diff --git a/src/tests/utils.test.jsx b/src/tests/utils.test.jsx new file mode 100644 index 0000000..19a240b --- /dev/null +++ b/src/tests/utils.test.jsx @@ -0,0 +1,63 @@ +import { filterHistoricData,truncateName } from '../domain/utils'; + +describe('utils', () => { + const mockHistoricData = { + '2025-06-01': { + commits: { + pau: 3, + lluis: 5, + total: 8, + anonymous: 1, + }, + modified_lines: { + pau: { modified: 100 }, + lluis: { modified: 200 }, + total: { modified: 300 }, + }, + }, + '2025-06-02': { + commits: { + pau: 4, + lluis: 6, + total: 10, + anonymous: 2, + }, + modified_lines: { + pau: { modified: 150 }, + lluis: { modified: 250 }, + total: { modified: 400 }, + }, + }, + }; + + test('filterHistoricData returns full data for "lifetime"', () => { + expect(filterHistoricData(mockHistoricData, 'lifetime')).toEqual(mockHistoricData); + }); + + test('filterHistoricData filters correctly by days', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2025-06-03')); + + const filtered = filterHistoricData(mockHistoricData, '1'); + expect(Object.keys(filtered)).toEqual(['2025-06-02']); + + vi.useRealTimers(); + }); + + + test('truncateName works correctly with long strings', () => { + + const string = truncateName("thisShouldBeTruncated"); + expect(string).toEqual("thisShouldBeTrunca..."); + + }); + + test('truncateName works correctly with short strings', () => { + + const string = truncateName("Pau"); + expect(string).toEqual("Pau"); + + }); +}); + +