A React dashboard for visualising engineering delivery metrics derived from Jira — cycle times, lead times, throughput, long-lived issues, production releases, and PR activity — across individuals and teams.
- Summary bar — team-wide median cycle time, total Jiras completed, production releases, and long-lived rate
- Engineer cards — per-engineer tile showing cycle/lead times, long-lived rate, Jiras completed, and releases; card border turns red when long-lived rate exceeds 20%
- Sort controls — sort by median cycle, long-lived %, Jiras completed, or prod releases (ascending/descending)
- Engineer modal — click any card to open a full-screen detail view containing:
- Cycle time distribution histogram (SVG, 4 buckets: <3d · 3–6d · 6–10d · >10d)
- Weekly throughput chart (SVG, last 8 weeks)
- In-progress issues table with age warnings (>7 days highlighted in red with ⚠)
- Recently completed issues with cycle time, lead time, and long-lived badge
- Production releases grouped by release name (expandable)
- PR activity (last 20 merged PRs)
- Filterable table — filter by assignee, epic, status, or long-lived flag
- Sortable columns — key, summary, assignee, epic, status, cycle time, lead time, created date
- Side panel — click any row to open a read-only detail panel showing dates, times, environment releases, and linked PRs
- Date range: last 7 / 30 / 60 / 90 days
- Window mode: issues started or completed in the selected range
- Team filter (Team tab)
- Project key filter (Jira tab)
- Dark / light theme (persisted to
localStorage)
| Layer | Library |
|---|---|
| UI framework | React 19 |
| State / caching | TanStack React Query v5 |
| Styling | Tailwind CSS v4 (Vite plugin) |
| Language | TypeScript 5.7 (strict) |
| Build | Vite 6 |
| Charts | Pure SVG (no chart library) |
# Install dependencies
npm install
# Start dev server (http://localhost:5173)
npm run dev
# Type-check + production build
npm run build
# Preview production build
npm run previewsrc/
├── api/ # React Query hooks (thin wrappers over mock/real API)
│ ├── engineers.ts # useEngineers()
│ ├── engineerStats.ts # useEngineerStats(filters)
│ ├── issues.ts # useIssues(filters)
│ └── epicStats.ts # useEpicStats(filters)
│
├── components/
│ ├── shell/ # App-level chrome
│ │ ├── DashboardShell.tsx # Root component — tab state, global filters
│ │ ├── DashboardHeader.tsx # Filter bar, tab buttons, theme toggle
│ │ ├── Skeleton.tsx # Animated loading placeholder
│ │ └── Tooltip.tsx # CSS-only hover tooltip
│ │
│ ├── team/ # Team tab
│ │ ├── TeamView.tsx # Data fetching, sort state, modal state
│ │ ├── TeamSummaryBar.tsx # Aggregated team metrics strip
│ │ ├── TeamSortControls.tsx # Sort key + direction controls
│ │ ├── EngineerGrid.tsx # Responsive card grid
│ │ ├── EngineerCard.tsx # Per-engineer summary tile
│ │ └── EngineerModal.tsx # Full-screen engineer detail modal
│ │
│ └── jira/ # Jira tab
│ ├── JiraView.tsx # Data fetching + local filter state
│ ├── JiraFilters.tsx # Assignee/epic/status/LL filter controls
│ ├── JiraTable.tsx # Sortable issues table (max 200 rows)
│ └── JiraSidePanel.tsx # Read-only issue detail panel
│
├── hooks/
│ ├── useFilterState.ts # Global dashboard filter state
│ └── useTheme.ts # Dark/light theme with localStorage persistence
│
├── lib/
│ └── utils.ts # formatDays, formatDate, cn, median, percentile75
│
├── mocks/ # Mock data and API simulation
│ ├── mockApi.ts # Central entry point, filterByWindow, 400ms delay
│ ├── engineers.ts # 12 engineers across 3 teams (Payments, Platform, Growth)
│ ├── issues.ts # 100+ issues across PAY, PLAT, GRW, CORE projects
│ ├── engineerStats.ts # Derives EngineerStats from issues
│ └── epicStats.ts # Derives EpicStats from issues
│
└── types/
└── index.ts # Shared TypeScript types
type JiraIssue = {
key: string;
summary: string;
assigneeId: string;
status: string; // "To Do" | "In Progress" | "Done"
createdAt: string; // ISO date
workStartedAt?: string; // when moved to In Progress
doneAt?: string;
cycleTimeDays?: number; // workStartedAt → doneAt
leadTimeDays?: number; // createdAt → doneAt
isLongLived: boolean; // cycleTimeDays > 10
releases: Release[]; // environments: sit | uat | preprod | prod
linkedPrs: PR[];
};
type EngineerStats = {
userId: string;
name: string;
jirasCompleted: number;
prodReleases: number;
medianCycleDays?: number;
p75CycleDays?: number;
medianLeadDays?: number;
longLivedCount: number;
longLivedRate: number; // 0–1, displayed as percentage
};
type DashboardFilters = {
range: 7 | 30 | 60 | 90; // days
mode: "started" | "completed";
teamId?: string;
projectKey?: string;
};All data is generated in src/mocks/. The USE_REAL_API flag in src/mocks/mockApi.ts is false by default; every API call goes through the mock layer with a simulated 400 ms network delay.
To wire up a real backend, set USE_REAL_API = true and implement the fetchX functions to call your actual endpoints. The React Query hooks in src/api/ will not need to change.
Mock dataset:
- 12 engineers across 3 teams (Payments, Platform, Growth)
- 100+ issues spread across projects PAY, PLAT, GRW, CORE
- Issues span the full 90-day look-back window
- Each issue includes realistic cycle/lead times, release pipeline entries (SIT → UAT → PREPROD → PROD), and linked PRs
| Metric | Definition |
|---|---|
| Cycle time | Time from workStartedAt (moved to In Progress) to doneAt |
| Lead time | Time from createdAt to doneAt — includes queue wait |
| Long-lived | Any issue whose cycle time exceeds 10 days |
| Long-lived rate | longLivedCount / jirasCompleted for the selected window |
| Prod releases | Distinct releases where environment === "prod" linked to the engineer's issues |
| Weekly throughput | Count of issues with doneAt falling in each of the last 8 calendar weeks |
| Hook | Stale time |
|---|---|
useEngineers |
∞ (static list) |
useEngineerStats |
5 minutes |
useEpicStats |
5 minutes |
useIssues |
2 minutes |
Re-opening an engineer modal within the 2-minute window is instant — React Query serves from cache without a network round-trip.