import React, { useState } from 'react';
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Download, RefreshCw, TrendingUp, CheckCircle, Clock, AlertCircle } from 'lucide-react';
const JiraEpicDashboard = () => {
const [jiraUrl, setJiraUrl] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [epicKey, setEpicKey] = useState('');
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState('');
const fetchEpicData = async () => {
setLoading(true);
setError('');
try {
const baseUrl = jiraUrl.replace(/\/$/, '');
const auth = btoa(`${username}:${password}`);
// First, get the epic details
const epicResponse = await fetch(`${baseUrl}/rest/api/3/issue/${epicKey}`, {
headers: {
'Authorization': `Basic ${auth}`,
'Accept': 'application/json'
}
});
if (!epicResponse.ok) {
throw new Error(`Failed to fetch epic: ${epicResponse.status}`);
}
const epicData = await epicResponse.json();
// Then get all issues in the epic
const jql = `"Epic Link" = ${epicKey} OR parent = ${epicKey}`;
const searchResponse = await fetch(
`${baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=1000&fields=summary,status,assignee,priority,timetracking,customfield_10016,issuetype`,
{
headers: {
'Authorization': `Basic ${auth}`,
'Accept': 'application/json'
}
}
);
if (!searchResponse.ok) {
throw new Error(`Failed to fetch issues: ${searchResponse.status}`);
}
const searchData = await searchResponse.json();
// Process the data
processData(epicData, searchData.issues);
} catch (err) {
setError(err.message);
console.error('Error fetching data:', err);
} finally {
setLoading(false);
}
};
const processData = (epic, issues) => {
const statusCounts = {};
const typeCounts = {};
const assigneeCounts = {};
let totalStoryPoints = 0;
let completedStoryPoints = 0;
let totalOriginalEstimate = 0;
let totalRemainingEstimate = 0;
let totalTimeSpent = 0;
issues.forEach(issue => {
const status = issue.fields.status.name;
const type = issue.fields.issuetype.name;
const assignee = issue.fields.assignee?.displayName || 'Unassigned';
// Count by status
statusCounts[status] = (statusCounts[status] || 0) + 1;
// Count by type
typeCounts[type] = (typeCounts[type] || 0) + 1;
// Count by assignee
assigneeCounts[assignee] = (assigneeCounts[assignee] || 0) + 1;
// Story points (customfield_10016 is common for story points, may vary)
const storyPoints = issue.fields.customfield_10016 || 0;
totalStoryPoints += storyPoints;
if (status === 'Done' || status === 'Closed') {
completedStoryPoints += storyPoints;
}
// Time tracking
if (issue.fields.timetracking) {
totalOriginalEstimate += issue.fields.timetracking.originalEstimateSeconds || 0;
totalRemainingEstimate += issue.fields.timetracking.remainingEstimateSeconds || 0;
totalTimeSpent += issue.fields.timetracking.timeSpentSeconds || 0;
}
});
const statusData = Object.entries(statusCounts).map(([name, value]) => ({
name,
value
}));
const typeData = Object.entries(typeCounts).map(([name, value]) => ({
name,
value
}));
const assigneeData = Object.entries(assigneeCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([name, value]) => ({
name,
value
}));
setData({
epicName: epic.fields.summary,
epicKey: epic.key,
totalIssues: issues.length,
statusData,
typeData,
assigneeData,
storyPoints: {
total: totalStoryPoints,
completed: completedStoryPoints,
remaining: totalStoryPoints - completedStoryPoints
},
timeTracking: {
originalEstimate: totalOriginalEstimate / 3600,
remaining: totalRemainingEstimate / 3600,
spent: totalTimeSpent / 3600
},
issues: issues.map(i => ({
key: i.key,
summary: i.fields.summary,
status: i.fields.status.name,
assignee: i.fields.assignee?.displayName || 'Unassigned',
type: i.fields.issuetype.name
}))
});
};
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D', '#FFC658', '#FF6B9D'];
const exportToCSV = () => {
if (!data) return;
const csv = [
['Key', 'Summary', 'Status', 'Assignee', 'Type'],
...data.issues.map(i => [i.key, i.summary, i.status, i.assignee, i.type])
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${data.epicKey}_export.csv`;
a.click();
};
if (!data) {
return (
Jira Epic Dashboard
setJiraUrl(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="https://jira.yourcompany.com"
/>
setUsername(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="DOMAIN\username"
/>
setPassword(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Your Windows password"
/>
setEpicKey(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="PROJ-123"
/>
{error && (
{error}
)}
);
}
const completionPercentage = data.storyPoints.total > 0
? Math.round((data.storyPoints.completed / data.storyPoints.total) * 100)
: 0;
return (
{data.epicName}
{data.epicKey}
Total Issues
{data.totalIssues}
Story Points
{data.storyPoints.total}
{data.storyPoints.completed} completed
Completion
{completionPercentage}%
Time Remaining
{data.timeTracking.remaining.toFixed(1)}h
{data.timeTracking.spent.toFixed(1)}h spent
Issues by Status
`${name}: ${value}`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{data.statusData.map((entry, index) => (
|
))}
Issues by Type
Top Assignees
All Issues
Key
Summary
Status
Assignee
Type
{data.issues.map((issue, index) => (
{issue.key}
{issue.summary}
{issue.status}
{issue.assignee}
{issue.type}
))}
);
};
export default JiraEpicDashboard;