[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:
parent
53698a4839
commit
347893a133
3 changed files with 513 additions and 512 deletions
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
*
|
||||
* TODO: REMOVE THIS 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"}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue