[Improve]Dashboard performance (#1989)

The logic behind this improvement is that, instead of fetching each
statistic individually, we aggregate all data from Postgres. This
reduces sequential fetches and multiple database calls.

How to test:
	1.	Connect to the production database.
	2.	Run npm run build and then npm start.
3. Go to the dashboard and try switch the organizations and personal
account.
4. Check if it loads quickly and verify that you are accessing the
correct statistics by comparing them with the deployed version. it
should take a maximum of 2–3 seconds .
video
https://codeflash-ai.slack.com/archives/C06BVLNRVT5/p1762480937547309

---------

Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
This commit is contained in:
HeshamHM28 2025-11-07 17:16:45 -08:00 committed by GitHub
parent 53698a4839
commit 347893a133
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 513 additions and 512 deletions

View file

@ -1,4 +0,0 @@
/**
*
* TODO: REMOVE THIS FILE
* **/

View file

@ -26,7 +26,7 @@ export interface RepositoryWithUsage {
optimizations_limit: number | null
optimizations_used: number
organization: string
membersCount?: number // Count from repository_members
membersCount?: number
avatarUrl?: string
}
@ -34,265 +34,323 @@ export async function getAllRepositories(
payload: GetRepositoriesForAccountPayload,
): Promise<RepositoryWithUsage[]> {
try {
const { repos, repoIds } = await getRepositoriesForAccountCached(payload)
if (repoIds.length === 0) return []
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
const repositories = (await getRepositoriesForAccountCached(payload)).repos
const activeRepoIds = await prisma.optimization_events.groupBy({
by: ["repository_id"],
where: {
repository_id: { in: repoIds },
created_at: { gte: thirtyDaysAgo },
},
})
// For each repo, check if it has any optimization event in the last 30 days
const reposWithActiveFlag = await Promise.all(
repositories.map(async repo => {
try {
const recentEventCount = await prisma.optimization_events.count({
where: {
repository_id: repo.id,
created_at: {
gte: thirtyDaysAgo,
},
},
})
const activeRepoSet = new Set(activeRepoIds.map(r => r.repository_id))
return {
id: repo.id,
github_repo_id: repo.github_repo_id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.is_private,
is_active: recentEventCount > 0,
has_github_action: repo.has_github_action,
created_at: repo.created_at,
last_optimized: repo.last_optimized,
optimizations_limit: repo.optimizations_limit,
optimizations_used: repo.optimizations_used,
organization: repo.full_name.split("/")[0],
avatarUrl: `https://github.com/${repo.full_name.split("/")[0]}.png`,
}
} catch (repoError) {
console.error(`Failed to process repository ${repo.id}:`, repoError)
// Return basic repo data on individual repo processing error
return {
id: repo.id,
github_repo_id: repo.github_repo_id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.is_private,
is_active: false,
has_github_action: repo.has_github_action,
created_at: repo.created_at,
last_optimized: repo.last_optimized,
optimizations_limit: repo.optimizations_limit,
optimizations_used: repo.optimizations_used,
organization: repo.full_name.split("/")[0],
avatarUrl: `https://github.com/${repo.full_name.split("/")[0]}.png`,
}
}
}),
)
const result = repos.map(repo => ({
id: repo.id,
github_repo_id: repo.github_repo_id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.is_private,
is_active: activeRepoSet.has(repo.id), // ✅ No DB check per repo
has_github_action: repo.has_github_action,
created_at: repo.created_at,
last_optimized: repo.last_optimized,
optimizations_limit: repo.optimizations_limit,
optimizations_used: repo.optimizations_used,
organization: repo.full_name.split("/")[0],
avatarUrl: `https://github.com/${repo.full_name.split("/")[0]}.png`,
}))
return reposWithActiveFlag
return result
} catch (error) {
console.error("Failed to fetch repositories:", error)
throw new Error(`Database error: ${error instanceof Error ? error.message : "Unknown error"}`)
}
}
// New wrapper functions that call the imported functions
export async function getUserOptimizationCount(payload: AccountPayload) {
function buildOptimizationWhereClause(
payload: AccountPayload,
repoIds: string[],
year?: number,
): string {
const repoIdsString = repoIds.map(id => `'${id}'`).join(",")
const yearCondition = year ? `AND EXTRACT(YEAR FROM created_at) = ${year}` : ""
if ("orgId" in payload) {
return `repository_id IN (${repoIdsString}) ${yearCondition}`
} else {
const userId = payload.userId.replace(/'/g, "''")
const username = payload.username.replace(/'/g, "''")
return `(
repository_id IN (${repoIdsString})
OR user_id = '${userId}'
OR current_username = '${username}'
) ${yearCondition}`
}
}
export async function statistics(payload: AccountPayload, year: number) {
try {
const { repoIds } = await getRepositoriesForAccountCached(payload)
return await getOptimizationEventCountByRepoIds(repoIds, payload)
} catch (error) {
console.error("Failed to get user optimization count:", error)
throw new Error(
`Failed to fetch optimization count: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getUserOptimizationSuccessfulCount(payload: AccountPayload) {
try {
return await getUserOptimizationSuccessfulCountForRepoIds(
(await getRepositoriesForAccountCached(payload)).repoIds,
payload,
)
} catch (error) {
console.error("Failed to get successful optimization count:", error)
throw new Error(
`Failed to fetch successful optimization count: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getActiveRepositoriesLast30Days(payload: AccountPayload) {
try {
return await getActiveRepoIdsLast30DaysByRepoIds(
(await getRepositoriesForAccountCached(payload)).repoIds,
payload,
)
} catch (error) {
console.error("Failed to get active repositories count:", error)
throw new Error(
`Failed to fetch active repositories: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getOptimizationsTimeSeriesData(
payload: AccountPayload,
onlySuccessful?: boolean,
) {
try {
const data = await getOptimizationsTimeSeriesDataByRepoIds(
(await getRepositoriesForAccountCached(payload)).repoIds,
payload,
onlySuccessful,
)
const groupedByDay: Record<string, number> = {}
data.forEach(item => {
// Use the user's local time zone to format the date as YYYY-MM-DD
const day = item.created_at
.toLocaleDateString(undefined, {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
})
.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2") // Convert MM/DD/YYYY to YYYY-MM-DD
groupedByDay[day] = (groupedByDay[day] || 0) + 1
})
const allDates = eachDayOfInterval({
start: new Date(Object.keys(groupedByDay).sort()[0]),
end: startOfDay(new Date()),
}).map(d =>
d
.toLocaleDateString(undefined, {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
})
.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2"),
)
let cumulativeCount = 0
const completeData = allDates.map(date => {
cumulativeCount += groupedByDay[date] || 0
return { date, count: cumulativeCount }
})
return completeData
} catch (error) {
console.error("Failed to fetch optimization time series data:", error)
throw new Error(
`Failed to fetch time series data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getPullRequestEventTimeSeriesData(
accountPayload: AccountPayload,
year: number,
) {
try {
if (!accountPayload) {
throw new Error("Invalid parameters provided")
}
const data = await getOptimizationEventsByRepoIds(
(await getRepositoriesForAccountCached(accountPayload)).repoIds,
{
forAccount: accountPayload,
eventTypes: ["pr_created", "pr_merged", "pr_closed"],
year,
select: {
event_type: true,
created_at: true,
},
},
)
const groupedByMonth: Record<string, Record<string, number>> = {}
// Initialize the months of the year
for (let month = 1; month <= 12; month++) {
const monthKey = `${year}-${month.toString().padStart(2, "0")}`
groupedByMonth[monthKey] = { pr_created: 0, pr_merged: 0, pr_closed: 0 }
if (repoIds.length === 0) {
return {
optimizations: { total: 0, successful: 0, timeSeries: [], successfulTimeSeries: [] },
activeUsersLast30Days: [],
pullRequests: [],
activeReposLast30Days: [],
}
}
data.forEach(item => {
const month = item.created_at.getMonth() + 1 // JavaScript months are 0-indexed
const monthKey = `${year}-${month.toString().padStart(2, "0")}`
if (groupedByMonth[monthKey]) {
groupedByMonth[monthKey][item.event_type] += 1
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
const whereClause = buildOptimizationWhereClause(payload, repoIds, year)
const sinceFormatted = since.toISOString()
const result = await prisma.$queryRawUnsafe<
Array<{
total_attempts: bigint
successful_attempts: bigint
daily_time_series: string
active_users: string
active_repos: string
pr_stats: string
}>
>(
`
WITH
-- Step 1: Filter and prepare base data with dynamic WHERE
base_events AS (
SELECT
created_at,
is_optimization_found,
current_username,
repository_id,
event_type,
DATE(created_at) as event_date,
created_at >= '${sinceFormatted}'::timestamp as is_recent,
EXTRACT(YEAR FROM created_at)::int = ${year} as is_target_year,
EXTRACT(MONTH FROM created_at)::int as event_month
FROM optimization_events
WHERE ${whereClause}
),
-- Step 2: Calculate total aggregates
totals AS (
SELECT
COUNT(*)::bigint as total_attempts,
SUM(CASE WHEN is_optimization_found THEN 1 ELSE 0 END)::bigint as successful_attempts
FROM base_events
),
-- Step 3: Daily time series with cumulative counts (WINDOW FUNCTIONS!)
daily_series AS (
SELECT
event_date,
COUNT(*) as daily_all,
SUM(CASE WHEN is_optimization_found THEN 1 ELSE 0 END) as daily_success,
SUM(COUNT(*)) OVER (
ORDER BY event_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) as cumulative_all,
SUM(SUM(CASE WHEN is_optimization_found THEN 1 ELSE 0 END)) OVER (
ORDER BY event_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) as cumulative_success
FROM base_events
GROUP BY event_date
ORDER BY event_date
),
-- Step 4: Active users (last 30 days)
active_users_agg AS (
SELECT
current_username,
COUNT(*)::bigint as event_count
FROM base_events
WHERE is_recent = true
AND current_username IS NOT NULL
GROUP BY current_username
ORDER BY event_count DESC
LIMIT 100
),
-- Step 5: Active repos (last 30 days)
active_repos_agg AS (
SELECT DISTINCT repository_id
FROM base_events
WHERE is_recent = true
AND repository_id IS NOT NULL
),
-- Step 6: PR stats by month
pr_stats_agg AS (
SELECT
event_month as month,
SUM(CASE WHEN event_type = 'pr_created' THEN 1 ELSE 0 END)::int as pr_created,
SUM(CASE WHEN event_type = 'pr_merged' THEN 1 ELSE 0 END)::int as pr_merged,
SUM(CASE WHEN event_type = 'pr_closed' THEN 1 ELSE 0 END)::int as pr_closed
FROM base_events
WHERE is_target_year = true
AND event_type IN ('pr_created', 'pr_merged', 'pr_closed')
GROUP BY event_month
),
-- Step 7: Aggregate time series into JSON
time_series_json AS (
SELECT
COALESCE(
json_agg(
json_build_object(
'date', event_date::text,
'all_count', cumulative_all,
'success_count', cumulative_success
) ORDER BY event_date
),
'[]'::json
) as daily_time_series
FROM daily_series
),
-- Step 8: Aggregate active users into JSON
users_json AS (
SELECT
COALESCE(
json_agg(
json_build_object(
'username', current_username,
'event_count', event_count
) ORDER BY event_count DESC
),
'[]'::json
) as active_users
FROM active_users_agg
),
-- Step 9: Aggregate active repos into JSON
repos_json AS (
SELECT
COALESCE(
json_agg(repository_id::text),
'[]'::json
) as active_repos
FROM active_repos_agg
),
-- Step 10: Aggregate PR stats into JSON
pr_json AS (
SELECT
COALESCE(
json_agg(
json_build_object(
'month', month,
'pr_created', pr_created,
'pr_merged', pr_merged,
'pr_closed', pr_closed
) ORDER BY month
),
'[]'::json
) as pr_stats
FROM pr_stats_agg
)
-- Final: Combine everything into single row
SELECT
COALESCE(t.total_attempts, 0) as total_attempts,
COALESCE(t.successful_attempts, 0) as successful_attempts,
ts.daily_time_series::text as daily_time_series,
u.active_users::text as active_users,
r.active_repos::text as active_repos,
p.pr_stats::text as pr_stats
FROM totals t
CROSS JOIN time_series_json ts
CROSS JOIN users_json u
CROSS JOIN repos_json r
CROSS JOIN pr_json p
`,
)
const data = result[0]
if (!data) {
return {
optimizations: { total: 0, successful: 0, timeSeries: [], successfulTimeSeries: [] },
activeUsersLast30Days: [],
pullRequests: [],
activeReposLast30Days: [],
}
}
const dailyData = JSON.parse(data.daily_time_series || "[]") as Array<{
date: string
all_count: number
success_count: number
}>
const activeUsersRaw = JSON.parse(data.active_users || "[]") as Array<{
username: string
event_count: number
}>
const activeReposRaw = JSON.parse(data.active_repos || "[]") as string[]
const prStatsRaw = JSON.parse(data.pr_stats || "[]") as Array<{
month: number
pr_created: number
pr_merged: number
pr_closed: number
}>
const optimizationTimeSeries = dailyData.map(d => ({
date: d.date,
count: d.all_count,
}))
const successfulTimeSeries = dailyData
.filter(d => d.success_count > 0)
.map(d => ({
date: d.date,
count: d.success_count,
}))
const activeUsersLast30Days = activeUsersRaw.map(u => ({
username: u.username,
eventCount: u.event_count,
avatarUrl: `https://github.com/${u.username}.png`,
}))
const prStatsMap = new Map(prStatsRaw.map(p => [p.month, p]))
const pullRequests = Array.from({ length: 12 }, (_, i) => {
const month = i + 1
const stats = prStatsMap.get(month)
return {
month: `${year}-${month.toString().padStart(2, "0")}`,
pr_created: stats?.pr_created || 0,
pr_merged: stats?.pr_merged || 0,
pr_closed: stats?.pr_closed || 0,
}
})
const completeData = Object.keys(groupedByMonth).map(monthKey => ({
month: monthKey,
pr_created: groupedByMonth[monthKey].pr_created,
pr_merged: groupedByMonth[monthKey].pr_merged,
pr_closed: groupedByMonth[monthKey].pr_closed,
}))
return completeData
return {
optimizations: {
total: Number(data.total_attempts),
successful: Number(data.successful_attempts),
timeSeries: optimizationTimeSeries,
successfulTimeSeries,
},
activeUsersLast30Days,
activeReposLast30Days: activeReposRaw,
pullRequests,
}
} catch (error) {
console.error("Failed to fetch pull request event time series data:", error)
console.error("Failed generating statistics:", error)
throw new Error(
`Failed to fetch PR time series data: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
export async function getActiveUserLeaderboardLast30Days(
payload: AccountPayload,
): Promise<{ username: string; eventCount: number; avatarUrl: string }[]> {
try {
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
const repoIds = (await getRepositoriesForAccountCached(payload)).repoIds
if (repoIds.length === 0) return []
const groupedCounts = await prisma.optimization_events.groupBy({
by: ["current_username"],
where: {
AND: [
{
repository_id: {
in: repoIds,
},
},
{
created_at: {
gte: since,
},
},
{
current_username: {
not: null,
},
},
],
},
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
},
},
})
return groupedCounts.map(entry => ({
username: entry.current_username!,
eventCount: entry._count.id,
avatarUrl: `https://github.com/${entry.current_username}.png`,
}))
} catch (error) {
console.error("Failed to fetch active user leaderboard:", error)
throw new Error(
`Failed to fetch leaderboard data: ${error instanceof Error ? error.message : "Unknown error"}`,
`Failed generating statistics: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}

View file

@ -1,18 +1,18 @@
"use client"
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useMemo, useEffect, useCallback } from "react"
import { Lock, Globe, RefreshCw, Zap, Gauge, FolderGit2, BookOpen } from "lucide-react"
import React, { useState, useMemo, useEffect, useCallback, memo } from "react"
import {
getAllRepositories,
RepositoryWithUsage,
getUserOptimizationCount,
getUserOptimizationSuccessfulCount,
getActiveRepositoriesLast30Days,
getOptimizationsTimeSeriesData,
getPullRequestEventTimeSeriesData,
getActiveUserLeaderboardLast30Days,
} from "./action"
Lock,
Globe,
RefreshCw,
Zap,
Gauge,
FolderGit2,
BookOpen,
CalendarDays,
ChevronDown,
} from "lucide-react"
import { getAllRepositories, RepositoryWithUsage, statistics } from "./action"
import { getUserIdAndUsername } from "@/app/utils/auth"
import { format, subDays } from "date-fns"
import { Loading } from "@/components/ui/loading"
@ -21,68 +21,63 @@ import { CompactPullRequestActivityCard } from "@/components/dashboard/CompactPu
import { DashboardErrorBoundary } from "@/components/dashboard/DashboardErrorBoundary"
import { MetricCard } from "@/components/dashboard/MetricCard"
import { useViewMode } from "../app/ViewModeContext"
import { useOutsideClick } from "@/components/hooks/useOutsideClick"
const getUserIdAndUsernameWithRetry = async (
retries = 3,
): Promise<{ userId: string; username: string }> => {
for (let i = 0; i < retries; i++) {
try {
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 500 * i))
}
const ErrorDisplay = memo(({ error, onRetry }: { error: string; onRetry: () => void }) => (
<div className="flex justify-center items-center h-[70vh]">
<div className="bg-red-50 text-red-800 p-6 sm:p-8 rounded-xl max-w-md border border-red-200">
<h3 className="text-base sm:text-lg font-medium mb-2 sm:mb-3">Unable to Load Dashboard</h3>
<p className="mb-3 sm:mb-4 text-sm sm:text-base">{error}</p>
<button
onClick={onRetry}
className="flex items-center gap-1 sm:gap-2 w-full justify-center px-3 sm:px-4 py-2 sm:py-2.5 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg text-xs sm:text-sm font-medium transition-colors"
>
<RefreshCw size={14} className="sm:w-4 sm:h-4" /> Try Again
</button>
</div>
</div>
))
ErrorDisplay.displayName = "ErrorDisplay"
const data = await getUserIdAndUsername()
if (!data || !data.userId || !data.username) {
if (i === retries - 1) {
throw new Error("No valid session found")
}
continue
}
return data
} catch (error) {
console.error(`Auth attempt ${i + 1} failed:`, error)
if (i === retries - 1) {
throw error
}
}
}
throw new Error("Authentication failed after all retries")
interface OptimizationStats {
totalAttempts: number
successfulAttempts: number
activeReposLast30Days: number
}
// Main dashboard component
const maxRetries = 3
interface PrActivityData {
month: string
pr_created: number
pr_merged: number
pr_closed: number
}
interface ActiveUserData {
username: string
eventCount: number
avatarUrl: string
}
function Dashboard() {
const { currentOrg } = useViewMode()
const currentYear = new Date().getFullYear()
const [repositories, setRepositories] = useState<RepositoryWithUsage[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [retryCount, setRetryCount] = useState(0)
const { currentOrg } = useViewMode()
const [optimizationStats, setOptimizationStats] = useState({
const [optimizationStats, setOptimizationStats] = useState<OptimizationStats>({
totalAttempts: 0,
successfulAttempts: 0,
activeReposLast30Days: 0,
})
const [, setTimeSeriesData] = useState<{ date: string; count: number }[]>([])
const [, setSuccessfulTimeSeriesData] = useState<{ date: string; count: number }[]>([])
const [prActivityData, setPrActivityData] = useState<
Array<{
month: string
pr_created: number
pr_merged: number
pr_closed: number
}>
>([])
const [selectedPrYear, setSelectedPrYear] = useState<number>(new Date().getFullYear())
const [activeUsersData, setActiveUsersData] = useState<
{ username: string; eventCount: number; avatarUrl: string }[]
>([])
const [prActivityData, setPrActivityData] = useState<PrActivityData[]>([])
const [selectedYear, setSelectedYear] = useState<number>(currentYear)
const [isYearDropdownOpen, setIsYearDropdownOpen] = useState(false)
const yearDropdownRef = useOutsideClick(() => setIsYearDropdownOpen(false))
const [activeUsersData, setActiveUsersData] = useState<ActiveUserData[]>([])
const [optimizationsTrend, setOptimizationsTrend] = useState<number[]>([])
const [optimizationsTrendDates, setOptimizationsTrendDates] = useState<string[]>([])
const [successfulOptimizationsTrend, setSuccessfulOptimizationsTrend] = useState<number[]>([])
@ -92,223 +87,172 @@ function Dashboard() {
const [isMobile, setIsMobile] = useState<boolean>(false)
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 640)
}
if (typeof window !== "undefined") {
handleResize()
window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize)
}
}, [])
const fetchDashboardData = useCallback(
async (attempt = 0) => {
try {
console.log({ attempt })
setLoading(attempt === 0)
setError(null)
if (attempt > 0) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000))
}
const currentUser = await getUserIdAndUsernameWithRetry()
if (!currentUser || !currentUser.userId || !currentUser.username) {
throw new Error("User authentication data not found")
}
const payload = currentOrg
? { orgId: currentOrg.id }
: { userId: currentUser.userId, username: currentUser.username }
const activeReposLast30Days = await getActiveRepositoriesLast30Days(payload)
const totalAttempts = await getUserOptimizationCount(payload)
const repos = await getAllRepositories(payload)
if (Array.isArray(repos)) {
setRepositories(repos)
} else {
console.warn("Received non-array repositories data:", repos)
setRepositories([])
}
const successfulAttempts = await getUserOptimizationSuccessfulCount(payload)
const optimizationsOverTime = await getOptimizationsTimeSeriesData(payload)
if (Array.isArray(optimizationsOverTime)) {
setTimeSeriesData(optimizationsOverTime)
} else {
setTimeSeriesData([])
}
const successfulOptimizationsOverTime = await getOptimizationsTimeSeriesData(payload, true)
if (Array.isArray(successfulOptimizationsOverTime)) {
setSuccessfulTimeSeriesData(successfulOptimizationsOverTime)
} else {
setSuccessfulTimeSeriesData([])
}
const leaderboardData = await getActiveUserLeaderboardLast30Days(
currentOrg
? { orgId: currentOrg.id }
: { userId: currentUser.userId, username: currentUser.username },
)
if (Array.isArray(leaderboardData)) {
setActiveUsersData(leaderboardData)
} else {
setActiveUsersData([])
}
const prData = await getPullRequestEventTimeSeriesData(
currentOrg
? { orgId: currentOrg.id }
: { userId: currentUser.userId, username: currentUser.username },
selectedPrYear,
)
if (Array.isArray(prData)) {
setPrActivityData(prData)
} else {
setPrActivityData([])
}
setOptimizationStats({
totalAttempts,
successfulAttempts,
activeReposLast30Days,
})
if (Array.isArray(optimizationsOverTime) && optimizationsOverTime.length > 0) {
const optimizationValues = optimizationsOverTime.map(item => item?.count || 0)
const optimizationDates = optimizationsOverTime.map(item => item?.date || "")
setOptimizationsTrend(optimizationValues)
setOptimizationsTrendDates(optimizationDates)
}
if (
Array.isArray(successfulOptimizationsOverTime) &&
successfulOptimizationsOverTime.length > 0
) {
const successfulValues = successfulOptimizationsOverTime.map(item => item?.count || 0)
const successfulDates = successfulOptimizationsOverTime.map(item => item?.date || "")
setSuccessfulOptimizationsTrend(successfulValues)
setSuccessfulOptimizationsTrendDates(successfulDates)
}
setRetryCount(0)
} catch (err) {
console.log(`Failed to fetch dashboard data (attempt ${attempt + 1}):`, err)
if (
attempt < maxRetries &&
err instanceof Error &&
(err.message.includes("authentication") ||
err.message.includes("User authentication data not found") ||
err.message.includes("Unauthorized") ||
err.message.includes("No valid session found"))
) {
setRetryCount(attempt + 1)
return fetchDashboardData(attempt + 1)
}
setError("Failed to load dashboard data. Please try again later.")
setRepositories([])
setPrActivityData([])
setActiveUsersData([])
setOptimizationsTrend([])
setOptimizationsTrendDates([])
setSuccessfulOptimizationsTrend([])
setSuccessfulOptimizationsTrendDates([])
} finally {
setLoading(false)
}
},
[selectedPrYear, currentOrg],
)
useEffect(() => {
const handleBeforeUnload = () => {
return null
}
window.addEventListener("beforeunload", handleBeforeUnload)
return () => window.removeEventListener("beforeunload", handleBeforeUnload)
}, [])
useEffect(() => {
// Simple initialization without localStorage timing checks
fetchDashboardData()
}, [fetchDashboardData])
const privateRepos = Array.isArray(repositories)
? repositories.filter(repo => repo?.is_private).length
: 0
const publicRepos = Array.isArray(repositories)
? repositories.filter(repo => !repo?.is_private).length
: 0
const now = useMemo(() => new Date(), [])
const last30DaysStart = subDays(now, 30)
const dateRangeDisplay = useMemo(() => {
const dateValues = useMemo(() => {
const now = new Date()
const last30DaysStart = subDays(now, 30)
const startMonth = format(last30DaysStart, "MMMM")
const endMonth = format(now, "MMMM")
const startYear = format(last30DaysStart, "yyyy")
const endYear = format(now, "yyyy")
let dateRangeDisplay: string
if (startMonth === endMonth && startYear === endYear) {
return `${startMonth} ${format(last30DaysStart, "d")}-${format(now, "d")}, ${startYear}`
dateRangeDisplay = `${startMonth} ${format(last30DaysStart, "d")}-${format(now, "d")}, ${startYear}`
} else if (startYear === endYear) {
return `${format(last30DaysStart, "MMMM d")} - ${format(now, "MMMM d")}, ${startYear}`
dateRangeDisplay = `${format(last30DaysStart, "MMMM d")} - ${format(now, "MMMM d")}, ${startYear}`
} else {
return `${format(last30DaysStart, "MMMM d, yyyy")} - ${format(now, "MMMM d, yyyy")}`
dateRangeDisplay = `${format(last30DaysStart, "MMMM d, yyyy")} - ${format(now, "MMMM d, yyyy")}`
}
}, [last30DaysStart, now])
if (loading) {
return <Loading />
}
return { now, last30DaysStart, dateRangeDisplay }
}, [])
if (error) {
return (
<div className="flex justify-center items-center h-[70vh]">
<div className="bg-red-50 text-red-800 p-6 sm:p-8 rounded-xl max-w-md border border-red-200">
<h3 className="text-base sm:text-lg font-medium mb-2 sm:mb-3">
Unable to Load Dashboard
</h3>
<p className="mb-3 sm:mb-4 text-sm sm:text-base">{error}</p>
{retryCount > 0 && (
<p className="mb-3 text-xs text-red-600">
Retry attempt: {retryCount}/{maxRetries}
</p>
)}
<button
onClick={() => fetchDashboardData()}
className="flex items-center gap-1 sm:gap-2 w-full justify-center px-3 sm:px-4 py-2 sm:py-2.5 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg text-xs sm:text-sm font-medium transition-colors"
>
<RefreshCw size={14} className="sm:w-4 sm:h-4" /> Try Again
</button>
</div>
</div>
)
}
const repoCounts = useMemo(() => {
if (!Array.isArray(repositories) || repositories.length === 0) {
return { privateRepos: 0, publicRepos: 0, totalRepos: 0 }
}
const privateRepos = repositories.filter(repo => repo?.is_private).length
const publicRepos = repositories.length - privateRepos
return { privateRepos, publicRepos, totalRepos: repositories.length }
}, [repositories])
const availableYears = useMemo(() => {
const baseYear = 2025
return Array.from(
{ length: Math.max(1, currentYear - baseYear + 1) },
(_, i) => baseYear + i,
).filter(year => year <= currentYear)
}, [currentYear])
useEffect(() => {
let timeoutId: NodeJS.Timeout
const handleResize = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => setIsMobile(window.innerWidth < 640), 150)
}
if (typeof window !== "undefined") {
setIsMobile(window.innerWidth < 640)
window.addEventListener("resize", handleResize)
return () => {
clearTimeout(timeoutId)
window.removeEventListener("resize", handleResize)
}
}
}, [])
const fetchDashboardData = useCallback(async () => {
try {
setLoading(true)
setError(null)
const currentUser = await getUserIdAndUsername()
if (!currentUser?.userId || !currentUser?.username) {
throw new Error("User authentication data not found")
}
const payload = currentOrg
? { orgId: currentOrg.id }
: { userId: currentUser.userId, username: currentUser.username }
const [stats, repos] = await Promise.all([
statistics(payload, selectedYear),
getAllRepositories(payload),
])
setRepositories(Array.isArray(repos) ? repos : [])
setOptimizationStats({
totalAttempts: stats.optimizations.total,
successfulAttempts: stats.optimizations.successful,
activeReposLast30Days: stats.activeReposLast30Days.length,
})
const optimizationValues = stats.optimizations.timeSeries.map(item => item.count)
const optimizationDates = stats.optimizations.timeSeries.map(item => item.date)
setOptimizationsTrend(optimizationValues)
setOptimizationsTrendDates(optimizationDates)
const successfulValues = stats.optimizations.successfulTimeSeries.map(item => item.count)
const successfulDates = stats.optimizations.successfulTimeSeries.map(item => item.date)
setSuccessfulOptimizationsTrend(successfulValues)
setSuccessfulOptimizationsTrendDates(successfulDates)
setPrActivityData(stats.pullRequests)
setActiveUsersData(stats.activeUsersLast30Days)
} catch (err) {
console.error("Dashboard data fetch error:", err)
setError("Failed to load dashboard data. Please try again later.")
setRepositories([])
setPrActivityData([])
setActiveUsersData([])
setOptimizationsTrend([])
setOptimizationsTrendDates([])
setSuccessfulOptimizationsTrend([])
setSuccessfulOptimizationsTrendDates([])
} finally {
setLoading(false)
}
}, [selectedYear, currentOrg])
useEffect(() => {
fetchDashboardData()
}, [fetchDashboardData])
const handleYearChange = useCallback((year: number) => {
setSelectedYear(year)
setIsYearDropdownOpen(false)
}, [])
if (loading) return <Loading />
if (error) return <ErrorDisplay error={error} onRetry={fetchDashboardData} />
return (
<div className="h-screen py-6 sm:py-8 px-4 sm:px-6 max-w-[1400px] mx-auto">
<div className="mb-6 sm:mb-8">
<div className="flex items-center mb-2">
<div className="flex items-center justify-between mb-2">
<h1 className="text-xl sm:text-2xl font-bold">Dashboard</h1>
<div className="relative" ref={yearDropdownRef}>
<button
onClick={() => setIsYearDropdownOpen(!isYearDropdownOpen)}
className="flex items-center gap-1 px-2 py-1 text-xs bg-background border border-border rounded-md hover:border-primary/50 transition-colors"
disabled={availableYears.length <= 1}
>
<CalendarDays size={12} className="text-muted-foreground" />
<span>{selectedYear}</span>
{availableYears.length > 1 && (
<ChevronDown
size={12}
className={`transition-transform text-muted-foreground ${isYearDropdownOpen ? "rotate-180" : ""}`}
/>
)}
</button>
{isYearDropdownOpen && availableYears.length > 1 && (
<div className="absolute right-0 z-10 mt-1 w-32 bg-card rounded-md shadow-lg overflow-hidden border border-border animate-in fade-in-50 slide-in-from-top-5">
<div className="py-1">
{availableYears.map(year => (
<button
key={year}
onClick={() => handleYearChange(year)}
className={`w-full px-3 py-1.5 text-left hover:bg-muted flex items-center ${selectedYear === year ? "bg-primary/10 text-primary font-medium" : ""}`}
>
<span className="w-4 h-4 mr-1.5 flex items-center justify-center">
{selectedYear === year && (
<span className="w-1.5 h-1.5 rounded-full bg-primary"></span>
)}
</span>
{year}
</button>
))}
</div>
</div>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-3 sm:gap-5 mb-6 sm:mb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-5">
<MetricCard
@ -322,14 +266,14 @@ function Dashboard() {
chartDates={optimizationsTrendDates}
chartColor="rgba(59, 130, 246, 1)"
chartFillColor="rgba(59, 130, 246, 0.2)"
timeText={dateRangeDisplay}
timeText={`Year ${selectedYear}`}
emptyStateMessage="No data"
cumulativeChart={true}
/>
<MetricCard
title="Optimizations Found"
value={optimizationStats.successfulAttempts}
subtitle={""}
subtitle=""
icon={<Gauge size={isMobile ? 16 : 20} />}
gradientFrom="bg-gradient-to-br from-emerald-500/20"
gradientTo="to-emerald-600/20"
@ -339,7 +283,7 @@ function Dashboard() {
chartColor="rgba(16, 185, 129, 1)"
chartFillColor="rgba(16, 185, 129, 0.2)"
emptyStateMessage="No data"
timeText="Last 30 days"
timeText={`Year ${selectedYear}`}
cumulativeChart={true}
showChart={successfulOptimizationsTrend.length > 0}
/>
@ -348,7 +292,7 @@ function Dashboard() {
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3 sm:gap-5">
<MetricCard
title="Total Repositories"
value={Array.isArray(repositories) ? repositories.length : 0}
value={repoCounts.totalRepos}
icon={<BookOpen size={isMobile ? 16 : 20} />}
gradientFrom="bg-gradient-to-br from-blue-500/20"
gradientTo="to-blue-600/20"
@ -365,13 +309,13 @@ function Dashboard() {
gradientFrom="bg-gradient-to-br from-purple-500/20"
gradientTo="to-purple-600/20"
iconColor="text-purple-500"
timeText={dateRangeDisplay}
timeText={dateValues.dateRangeDisplay}
showChart={false}
/>
<MetricCard
title="Private Repositories"
value={privateRepos}
value={repoCounts.privateRepos}
icon={<Lock size={isMobile ? 16 : 20} />}
gradientFrom="bg-gradient-to-br from-amber-500/20"
gradientTo="to-amber-600/20"
@ -382,7 +326,7 @@ function Dashboard() {
<MetricCard
title="Public Repositories"
value={publicRepos}
value={repoCounts.publicRepos}
icon={<Globe size={isMobile ? 16 : 20} />}
gradientFrom="bg-gradient-to-br from-violet-500/20"
gradientTo="to-violet-600/20"
@ -396,8 +340,8 @@ function Dashboard() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-5 mb-6 sm:mb-8 h-96 md:h-[500px]">
<CompactPullRequestActivityCard
prData={prActivityData}
selectedYear={selectedPrYear}
onYearChange={setSelectedPrYear}
selectedYear={selectedYear}
onYearChange={handleYearChange}
className="h-full"
/>
@ -409,10 +353,13 @@ function Dashboard() {
)
}
const MemoizedDashboard = memo(Dashboard)
MemoizedDashboard.displayName = "Dashboard"
export default function DashboardWrapper() {
return (
<DashboardErrorBoundary>
<Dashboard />
<MemoizedDashboard />
</DashboardErrorBoundary>
)
}