Initial commit
This commit is contained in:
283
src/components/Dashboard.tsx
Normal file
283
src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { RefreshCw, AlertCircle, Settings, Activity } from 'lucide-react';
|
||||
import { WorkflowRun } from '../types/github';
|
||||
import { ApiService } from '../services/api';
|
||||
import { CompactWorkflowCard } from './CompactWorkflowCard';
|
||||
import { WorkflowRunsModal } from './WorkflowRunsModal';
|
||||
import { ThemeSwitcher } from './ThemeSwitcher';
|
||||
import { SettingsModal } from './SettingsModal';
|
||||
import { ApiStatusModal } from './ApiStatusModal';
|
||||
import { useSettings } from '../contexts/SettingsContext';
|
||||
import { NotificationService } from '../services/notifications';
|
||||
|
||||
interface DashboardProps {}
|
||||
|
||||
export const Dashboard: React.FC<DashboardProps> = () => {
|
||||
const [workflowRuns, setWorkflowRuns] = useState<WorkflowRun[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const [repositoryCount, setRepositoryCount] = useState(0);
|
||||
const [selectedRepository, setSelectedRepository] = useState<{owner: string, name: string} | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [isApiStatusOpen, setIsApiStatusOpen] = useState(false);
|
||||
|
||||
const { settings } = useSettings();
|
||||
const previousWorkflowRuns = useRef<WorkflowRun[]>([]);
|
||||
const notificationService = useMemo(() => NotificationService.getInstance(), []);
|
||||
|
||||
const apiService = useMemo(() => new ApiService(), []);
|
||||
|
||||
const fetchWorkflowRuns = useCallback(async (isAutoRefresh = false) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const runs = await apiService.getWorkflowRuns();
|
||||
|
||||
// Check for notifications if this is an auto-refresh and notifications are enabled
|
||||
if (isAutoRefresh && settings.notifications.enabled && previousWorkflowRuns.current.length > 0) {
|
||||
const previousRuns = previousWorkflowRuns.current;
|
||||
const currentRuns = runs;
|
||||
|
||||
// Find new failures
|
||||
if (settings.notifications.showFailures) {
|
||||
const newFailures = currentRuns.filter(currentRun =>
|
||||
currentRun.conclusion === 'failure' &&
|
||||
!previousRuns.some(prevRun =>
|
||||
prevRun.id === currentRun.id &&
|
||||
prevRun.conclusion === 'failure'
|
||||
)
|
||||
);
|
||||
|
||||
if (newFailures.length > 0) {
|
||||
notificationService.showFailureNotification(newFailures);
|
||||
}
|
||||
}
|
||||
|
||||
// Find new recoveries (previously failed runs that are now successful)
|
||||
if (settings.notifications.showRecoveries) {
|
||||
const newRecoveries = currentRuns.filter(currentRun =>
|
||||
currentRun.conclusion === 'success' &&
|
||||
previousRuns.some(prevRun =>
|
||||
prevRun.repository.full_name === currentRun.repository.full_name &&
|
||||
prevRun.conclusion === 'failure'
|
||||
) &&
|
||||
!previousRuns.some(prevRun =>
|
||||
prevRun.id === currentRun.id &&
|
||||
prevRun.conclusion === 'success'
|
||||
)
|
||||
);
|
||||
|
||||
if (newRecoveries.length > 0) {
|
||||
notificationService.showSuccessNotification(newRecoveries);
|
||||
}
|
||||
}
|
||||
|
||||
// Find new waiting runs
|
||||
if (settings.notifications.showWaiting) {
|
||||
const newWaitingRuns = currentRuns.filter(currentRun =>
|
||||
currentRun.status === 'waiting' &&
|
||||
!previousRuns.some(prevRun =>
|
||||
prevRun.id === currentRun.id &&
|
||||
prevRun.status === 'waiting'
|
||||
)
|
||||
);
|
||||
|
||||
if (newWaitingRuns.length > 0) {
|
||||
notificationService.showWaitingNotification(newWaitingRuns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousWorkflowRuns.current = runs;
|
||||
setWorkflowRuns(runs);
|
||||
setLastUpdated(new Date());
|
||||
} catch (error) {
|
||||
console.error('Error fetching workflow runs:', error);
|
||||
setError('Failed to fetch workflow runs. Please check your configuration.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [apiService, settings.notifications, notificationService]);
|
||||
|
||||
const fetchConfig = useCallback(async () => {
|
||||
try {
|
||||
const config = await apiService.getConfig();
|
||||
setRepositoryCount(config.repositories.length);
|
||||
} catch (error) {
|
||||
console.error('Error fetching config:', error);
|
||||
}
|
||||
}, [apiService]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchWorkflowRuns();
|
||||
}, [fetchConfig, fetchWorkflowRuns]);
|
||||
|
||||
// Request notification permission when notifications are enabled
|
||||
useEffect(() => {
|
||||
if (settings.notifications.enabled) {
|
||||
notificationService.requestPermission();
|
||||
}
|
||||
}, [settings.notifications.enabled, notificationService]);
|
||||
|
||||
// Auto-refresh functionality
|
||||
useEffect(() => {
|
||||
if (!settings.autoRefreshInterval) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchWorkflowRuns(true);
|
||||
}, settings.autoRefreshInterval * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [settings.autoRefreshInterval, fetchWorkflowRuns]);
|
||||
|
||||
|
||||
const statusCounts = {
|
||||
total: workflowRuns.length,
|
||||
success: workflowRuns.filter(r => r.conclusion === 'success').length,
|
||||
failure: workflowRuns.filter(r => r.conclusion === 'failure').length,
|
||||
active: workflowRuns.filter(r =>
|
||||
r.status === 'in_progress' ||
|
||||
r.status === 'queued'
|
||||
).length,
|
||||
waiting: workflowRuns.filter(r => r.status === 'queued').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-ctp-base">
|
||||
<div className="bg-ctp-mantle shadow-sm border-b border-ctp-surface0">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-xl font-semibold text-ctp-text">GitHub Actions Radar</h1>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-1">
|
||||
<p className="text-sm text-ctp-subtext0">Latest main branch runs</p>
|
||||
{settings.showLastUpdateTime && lastUpdated && (
|
||||
<p className="text-xs text-ctp-subtext1">
|
||||
Last updated: {lastUpdated.toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
{settings.githubUsername && (
|
||||
<p className="text-xs text-ctp-subtext1 truncate">
|
||||
Welcome, {settings.githubUsername}!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 md:space-x-4 ml-4">
|
||||
<ThemeSwitcher />
|
||||
<button
|
||||
onClick={() => setIsApiStatusOpen(true)}
|
||||
className="flex items-center space-x-2 px-2 md:px-3 py-2 bg-ctp-mantle border border-ctp-surface0 rounded-lg hover:bg-ctp-surface0 transition-colors"
|
||||
>
|
||||
<Activity className="w-4 h-4 text-ctp-text" />
|
||||
<span className="text-sm text-ctp-text hidden sm:inline">API Status</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
className="flex items-center space-x-2 px-2 md:px-3 py-2 bg-ctp-mantle border border-ctp-surface0 rounded-lg hover:bg-ctp-surface0 transition-colors"
|
||||
>
|
||||
<Settings className="w-4 h-4 text-ctp-text" />
|
||||
<span className="text-sm text-ctp-text hidden sm:inline">Settings</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={fetchWorkflowRuns}
|
||||
disabled={loading}
|
||||
className="flex items-center space-x-2 px-3 md:px-4 py-2 bg-ctp-blue text-ctp-base rounded-lg hover:bg-ctp-lavender disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span className="hidden sm:inline">Refresh</span>
|
||||
</button>
|
||||
<span className="text-sm text-ctp-subtext0 hidden md:inline">
|
||||
{repositoryCount} repositories
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
|
||||
<div className="bg-ctp-mantle rounded-lg shadow-sm border border-ctp-surface0 p-6">
|
||||
<div className="text-2xl font-bold text-ctp-text">{statusCounts.total}</div>
|
||||
<div className="text-sm text-ctp-subtext0">Total Runs</div>
|
||||
</div>
|
||||
<div className="bg-ctp-mantle rounded-lg shadow-sm border border-ctp-surface0 p-6">
|
||||
<div className="text-2xl font-bold text-ctp-green">{statusCounts.success}</div>
|
||||
<div className="text-sm text-ctp-subtext0">Successful</div>
|
||||
</div>
|
||||
<div className="bg-ctp-mantle rounded-lg shadow-sm border border-ctp-surface0 p-6">
|
||||
<div className="text-2xl font-bold text-ctp-red">{statusCounts.failure}</div>
|
||||
<div className="text-sm text-ctp-subtext0">Failed</div>
|
||||
</div>
|
||||
<div className="bg-ctp-mantle rounded-lg shadow-sm border border-ctp-surface0 p-6">
|
||||
<div className="text-2xl font-bold text-ctp-blue">{statusCounts.active}</div>
|
||||
<div className="text-sm text-ctp-subtext0">Active</div>
|
||||
</div>
|
||||
<div className="bg-ctp-mantle rounded-lg shadow-sm border border-ctp-surface0 p-6">
|
||||
<div className="text-2xl font-bold text-ctp-rosewater">{statusCounts.waiting}</div>
|
||||
<div className="text-sm text-ctp-subtext0">Waiting</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{error && (
|
||||
<div className="bg-ctp-mantle border border-ctp-red/30 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertCircle className="w-5 h-5 text-ctp-red" />
|
||||
<p className="text-ctp-red">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{loading ? (
|
||||
<div className="col-span-full flex justify-center py-12">
|
||||
<RefreshCw className="w-8 h-8 animate-spin text-ctp-blue" />
|
||||
</div>
|
||||
) : workflowRuns.length === 0 ? (
|
||||
<div className="col-span-full text-center py-12">
|
||||
<p className="text-ctp-subtext0">No workflow runs found.</p>
|
||||
<p className="text-sm text-ctp-subtext1 mt-2">Check your config.json file</p>
|
||||
</div>
|
||||
) : (
|
||||
workflowRuns.map(run => (
|
||||
<CompactWorkflowCard
|
||||
key={run.id}
|
||||
run={run}
|
||||
onClick={() => {
|
||||
setSelectedRepository({
|
||||
owner: run.repository.owner.login,
|
||||
name: run.repository.name
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WorkflowRunsModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedRepository(null);
|
||||
}}
|
||||
repository={selectedRepository}
|
||||
/>
|
||||
|
||||
<SettingsModal
|
||||
isOpen={isSettingsOpen}
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
/>
|
||||
|
||||
<ApiStatusModal
|
||||
isOpen={isApiStatusOpen}
|
||||
onClose={() => setIsApiStatusOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user