diff --git a/package-lock.json b/package-lock.json index 899910b..71303c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -434,7 +435,8 @@ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251219.0.tgz", "integrity": "sha512-qwuvc3ZDdCfcK9dJrBSFHOsX8kL72sypfBilzEWbbb+slB2NiggjsHeGMV2ZQiQc1zyBMQPjIvsVeE7Apxp7hw==", "dev": true, - "license": "MIT OR Apache-2.0" + "license": "MIT OR Apache-2.0", + "peer": true }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -1039,6 +1041,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.6.tgz", "integrity": "sha512-4uyt8BOrBsSq6i4yiOV/gG6BnnrvTeyymlNcaN/dKvyU1GoolxAafvIvaNP1RCGPlNab3OuE4MKUQuv2lH+PLQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", @@ -1105,6 +1108,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.6.tgz", "integrity": "sha512-YYGARbutghQY4zZUWMYia0ib0Y/rb52y72/N0z3vglRHL7ii/AaK9SA7S/dzScVOlCdnbHXz+sc5Dq+r8fwFAg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.14.6", "@firebase/component": "0.7.0", @@ -1120,7 +1124,8 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/auth": { "version": "1.12.0", @@ -1571,6 +1576,7 @@ "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2898,6 +2904,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3122,6 +3129,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3321,6 +3329,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3565,8 +3574,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", @@ -6305,6 +6313,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6314,6 +6323,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6360,6 +6370,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -6422,7 +6433,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -6566,6 +6578,7 @@ "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7042,6 +7055,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7090,6 +7104,7 @@ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "pathe": "^2.0.3" } @@ -7308,6 +7323,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -7441,6 +7457,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "bin": { "workerd": "bin/workerd" }, diff --git a/services/cacheService.ts b/services/cacheService.ts index d862ba9..2fac5c2 100644 --- a/services/cacheService.ts +++ b/services/cacheService.ts @@ -68,6 +68,8 @@ export const CacheKeys = { prDetails: (owner: string, repo: string, prNumber: number) => `pr_${owner}_${repo}_${prNumber}`, issueExpandedData: (owner: string, repo: string, issueNumber: number) => `expanded_${owner}_${repo}_${issueNumber}`, workflowFiles: () => 'workflow_files', + repoPullRequests: (owner: string, repo: string) => `pulls_${owner}_${repo}`, + repoDeployments: (owner: string, repo: string) => `deployments_${owner}_${repo}`, }; // Type for cached expanded issue data (all data needed for expanded view) diff --git a/services/githubService.ts b/services/githubService.ts index 663ef21..d6d817e 100644 --- a/services/githubService.ts +++ b/services/githubService.ts @@ -19,6 +19,38 @@ export const validateToken = async (token: string): Promise => { return response.json(); }; +export const fetchPullRequests = async (token: string, owner: string, repo: string): Promise => { + const response = await fetch(`${GITHUB_API_BASE}/repos/${owner}/${repo}/pulls?state=all&per_page=30&sort=updated&direction=desc`, { + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + cache: 'no-cache', + }); + + if (!response.ok) { + throw new Error('Failed to fetch pull requests'); + } + + return response.json(); +}; + +export const fetchDeployments = async (token: string, owner: string, repo: string): Promise => { + const response = await fetch(`${GITHUB_API_BASE}/repos/${owner}/${repo}/deployments?per_page=30`, { + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + cache: 'no-cache', + }); + + if (!response.ok) { + throw new Error('Failed to fetch deployments'); + } + + return response.json(); +}; + export const fetchRepositories = async (token: string, page = 1): Promise => { const response = await fetch(`${GITHUB_API_BASE}/user/repos?sort=updated&per_page=12&page=${page}`, { headers: { diff --git a/views/Dashboard.tsx b/views/Dashboard.tsx index 486e2c6..e0f1d5b 100644 --- a/views/Dashboard.tsx +++ b/views/Dashboard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { Repository, GitHubUser, RepoDraft, Issue, GitHubTemplate } from '../types'; -import { fetchRepositories, createRepository, deleteRepository, fetchGitHubTemplates } from '../services/githubService'; +import { fetchRepositories, createRepository, deleteRepository, fetchGitHubTemplates, fetchIssues, fetchPullRequests, fetchWorkflowRuns, fetchDeployments } from '../services/githubService'; import { completeRepositorySetup } from '../services/repoSetupUtils'; import { RepoCard } from '../components/RepoCard'; import { Button } from '../components/Button'; @@ -142,23 +142,37 @@ export const Dashboard: React.FC = ({ token, user, onRepoSelect, // Cache the repos for instant display on next visit setCache(CacheKeys.repos(), data); - // Load issues for first 4 repos - reuse cache when available - const reposToShow = data.slice(0, 4); + // Pre-fetch issues, PRs, actions, and deployments for the top 4 repos + const reposToPreload = data.slice(0, 4); const issuesMap: Record = {}; - - for (const repo of reposToShow) { - const cacheKey = CacheKeys.repoIssues(repo.owner.login, repo.name); - const cachedIssues = getCached(cacheKey); - - if (cachedIssues) { - // Reuse cached issues - filter to actual issues (not PRs) and take first 3 - const actualIssues = cachedIssues.filter(issue => !issue.pull_request).slice(0, 3); + + await Promise.allSettled(reposToPreload.map(async (repo) => { + try { + // Fetch all data in parallel + const [issues, pullRequests, workflowRuns, deployments] = await Promise.all([ + fetchIssues(token, repo.owner.login, repo.name), + fetchPullRequests(token, repo.owner.login, repo.name), + fetchWorkflowRuns(token, repo.owner.login, repo.name), + fetchDeployments(token, repo.owner.login, repo.name), + ]); + + // Cache each dataset + setCache(CacheKeys.repoIssues(repo.owner.login, repo.name), issues); + setCache(CacheKeys.repoPullRequests(repo.owner.login, repo.name), pullRequests); + setCache(CacheKeys.workflowRuns(repo.owner.login, repo.name), workflowRuns); + setCache(CacheKeys.repoDeployments(repo.owner.login, repo.name), deployments); + + // Update the local state for immediate UI update (for issues) + const actualIssues = issues.filter(issue => !issue.pull_request).slice(0, 3); issuesMap[repo.id] = actualIssues; + + } catch (error) { + // Log errors but don't block the UI + console.error(`Failed to preload data for ${repo.name}:`, error); } - // If not cached, leave empty - issues will be cached when user visits repo detail - } + })); - setRepoIssues(issuesMap); + setRepoIssues(prev => ({ ...prev, ...issuesMap })); } catch (err) { // Only show error if we don't have cached data to display if (!hasCachedData) { diff --git a/views/RepoDetail.tsx b/views/RepoDetail.tsx index 7982255..d3d82cc 100644 --- a/views/RepoDetail.tsx +++ b/views/RepoDetail.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; -import { Repository, Issue, WorkflowFile, RepoSecret } from '../types'; -import { fetchIssues, createIssue, fetchAllWorkflowFiles, fetchRepositorySecrets, setRepositorySecret, deleteRepositorySecret, createIssueComment } from '../services/githubService'; +import { Repository, Issue, WorkflowFile, RepoSecret, WorkflowRun, Deployment } from '../types'; +import { fetchIssues, createIssue, fetchAllWorkflowFiles, fetchRepositorySecrets, setRepositorySecret, deleteRepositorySecret, createIssueComment, fetchPullRequests, fetchWorkflowRuns, fetchDeployments } from '../services/githubService'; import { autoSetAllTokens, setupRepositoryWorkflows } from '../services/repoSetupUtils'; import { Button } from '../components/Button'; import { ToastContainer, useToast } from '../components/Toast'; @@ -16,15 +16,17 @@ interface RepoDetailProps { export const RepoDetail: React.FC = ({ token, repo, onBack, onIssueSelect }) => { const { toasts, dismissToast, showError } = useToast(); - const cacheKey = CacheKeys.repoIssues(repo.owner.login, repo.name); - // Initialize from cache for instant display - const [issues, setIssues] = useState(() => { - return getCached(cacheKey) || []; - }); + // Initialize all data from cache for instant display + const [issues, setIssues] = useState(() => getCached(CacheKeys.repoIssues(repo.owner.login, repo.name)) || []); + const [pullRequests, setPullRequests] = useState(() => getCached(CacheKeys.repoPullRequests(repo.owner.login, repo.name)) || []); + const [workflowRuns, setWorkflowRuns] = useState(() => getCached(CacheKeys.workflowRuns(repo.owner.login, repo.name)) || []); + const [deployments, setDeployments] = useState(() => getCached(CacheKeys.repoDeployments(repo.owner.login, repo.name)) || []); + const [loading, setLoading] = useState(() => { - // Only show loading if no cached data - return !getCached(cacheKey); + // Show loading if any of the primary data is not cached + return !getCached(CacheKeys.repoIssues(repo.owner.login, repo.name)) || + !getCached(CacheKeys.repoPullRequests(repo.owner.login, repo.name)); }); const [isRefreshing, setIsRefreshing] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); @@ -81,10 +83,9 @@ export const RepoDetail: React.FC = ({ token, repo, onBack, onI // Filter out pull requests from the main list const issuesOnly = issues.filter(issue => !issue.pull_request); - const loadIssues = React.useCallback(async (isManualRefresh = false) => { - const hasCachedData = issues.length > 0; + const loadRepoData = React.useCallback(async (isManualRefresh = false) => { + const hasCachedData = issues.length > 0 || pullRequests.length > 0; - // Show full loading only on first load with no cache if (!hasCachedData) { setLoading(true); } else if (isManualRefresh) { @@ -92,21 +93,38 @@ export const RepoDetail: React.FC = ({ token, repo, onBack, onI } try { - const data = await fetchIssues(token, repo.owner.login, repo.name); - setIssues(data); - // Cache the issues for instant display on next visit - setCache(cacheKey, data); + // Fetch all data in parallel + const [fetchedIssues, fetchedPrs, fetchedRuns, fetchedDeployments] = await Promise.all([ + fetchIssues(token, repo.owner.login, repo.name), + fetchPullRequests(token, repo.owner.login, repo.name), + fetchWorkflowRuns(token, repo.owner.login, repo.name), + fetchDeployments(token, repo.owner.login, repo.name), + ]); + + // Update state + setIssues(fetchedIssues); + setPullRequests(fetchedPrs); + setWorkflowRuns(fetchedRuns); + setDeployments(fetchedDeployments); + + // Cache all the data for next time + setCache(CacheKeys.repoIssues(repo.owner.login, repo.name), fetchedIssues); + setCache(CacheKeys.repoPullRequests(repo.owner.login, repo.name), fetchedPrs); + setCache(CacheKeys.workflowRuns(repo.owner.login, repo.name), fetchedRuns); + setCache(CacheKeys.repoDeployments(repo.owner.login, repo.name), fetchedDeployments); + } catch (err) { console.error(err); + showError("Failed to load repository data."); } finally { setLoading(false); setIsRefreshing(false); } - }, [token, repo, cacheKey, issues.length]); + }, [token, repo, issues.length, pullRequests.length, showError]); useEffect(() => { // Always fetch fresh data on mount, but show cached immediately - loadIssues(false); + loadRepoData(false); }, []); // eslint-disable-line react-hooks/exhaustive-deps const handleCreateIssue = async (e: React.FormEvent) => { @@ -301,7 +319,7 @@ export const RepoDetail: React.FC = ({ token, repo, onBack, onI {isRefreshing && ( Updating... )} -