Enhance frontend dashboard with stats, modals, and actions
All checks were successful
Build and Push Frontend / build (push) Successful in 16s

- Add StatsCards component with key metrics (projects, tasks, agents)
- Add CreateProjectModal for creating new projects
- Add CreateTaskModal for creating new tasks
- Add action buttons in dashboard header
- Improve Dashboard with stats and modal integration
- All components with proper TypeScript types

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hector Ros
2026-01-20 02:02:24 +01:00
parent 90615832dd
commit 8b5f75a289
4 changed files with 345 additions and 3 deletions

View File

@@ -0,0 +1,108 @@
import React, { useState } from 'react'
import apiClient from '../api/client'
interface CreateProjectModalProps {
isOpen: boolean
onClose: () => void
onCreated: () => void
}
export default function CreateProjectModal({ isOpen, onClose, onCreated }: CreateProjectModalProps) {
const [formData, setFormData] = useState({
name: '',
description: '',
giteaOwner: 'admin',
giteaRepoName: '',
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
await apiClient.post('/projects', formData)
onCreated()
onClose()
setFormData({ name: '', description: '', giteaOwner: 'admin', giteaRepoName: '' })
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to create project')
} finally {
setLoading(false)
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-8 max-w-md w-full">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Create New Project</h2>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Project Name *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gitea Repository Name *
</label>
<input
type="text"
value={formData.giteaRepoName}
onChange={(e) => setFormData({ ...formData, giteaRepoName: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="my-project"
required
/>
</div>
<div className="flex space-x-3 mt-6">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Creating...' : 'Create Project'}
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -0,0 +1,127 @@
import React, { useState } from 'react'
import apiClient from '../api/client'
import type { Project } from '../types'
interface CreateTaskModalProps {
isOpen: boolean
onClose: () => void
onCreated: () => void
projects: Project[]
}
export default function CreateTaskModal({ isOpen, onClose, onCreated, projects }: CreateTaskModalProps) {
const [formData, setFormData] = useState({
projectId: '',
title: '',
description: '',
priority: 'medium',
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
await apiClient.post('/tasks', formData)
onCreated()
onClose()
setFormData({ projectId: '', title: '', description: '', priority: 'medium' })
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to create task')
} finally {
setLoading(false)
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-8 max-w-md w-full">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Create New Task</h2>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Project *</label>
<select
value={formData.projectId}
onChange={(e) => setFormData({ ...formData, projectId: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">Select a project</option>
{projects.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Task Title *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Add authentication to API"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
placeholder="Implement JWT-based authentication with session management..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Priority</label>
<select
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div className="flex space-x-3 mt-6">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Creating...' : 'Create Task'}
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -0,0 +1,71 @@
import React from 'react'
import type { Project, Task, Agent } from '../types'
interface StatsCardsProps {
projects: Project[]
tasks: Task[]
agents: Agent[]
}
export default function StatsCards({ projects, tasks, agents }: StatsCardsProps) {
const activeProjects = projects.filter((p) => p.status === 'active').length
const activeTasks = tasks.filter((t) => ['backlog', 'in_progress', 'needs_input', 'ready_to_test'].includes(t.state)).length
const idleAgents = agents.filter((a) => a.status === 'idle').length
const busyAgents = agents.filter((a) => a.status === 'busy').length
const stats = [
{
name: 'Active Projects',
value: activeProjects,
total: projects.length,
color: 'blue',
},
{
name: 'Active Tasks',
value: activeTasks,
total: tasks.length,
color: 'purple',
},
{
name: 'Idle Agents',
value: idleAgents,
total: agents.length,
color: 'green',
},
{
name: 'Busy Agents',
value: busyAgents,
total: agents.length,
color: 'orange',
},
]
const getColorClasses = (color: string) => {
switch (color) {
case 'blue':
return 'bg-blue-50 text-blue-700 border-blue-200'
case 'purple':
return 'bg-purple-50 text-purple-700 border-purple-200'
case 'green':
return 'bg-green-50 text-green-700 border-green-200'
case 'orange':
return 'bg-orange-50 text-orange-700 border-orange-200'
default:
return 'bg-gray-50 text-gray-700 border-gray-200'
}
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{stats.map((stat) => (
<div key={stat.name} className={`border rounded-lg p-6 ${getColorClasses(stat.color)}`}>
<p className="text-sm font-medium">{stat.name}</p>
<p className="mt-2 text-3xl font-semibold">
{stat.value}
<span className="text-lg text-gray-600">/{stat.total}</span>
</p>
</div>
))}
</div>
)
}

View File

@@ -4,6 +4,9 @@ import type { Project, Task, Agent } from '../types'
import ProjectList from '../components/ProjectList' import ProjectList from '../components/ProjectList'
import TaskList from '../components/TaskList' import TaskList from '../components/TaskList'
import AgentStatus from '../components/AgentStatus' import AgentStatus from '../components/AgentStatus'
import StatsCards from '../components/StatsCards'
import CreateProjectModal from '../components/CreateProjectModal'
import CreateTaskModal from '../components/CreateTaskModal'
export default function Dashboard() { export default function Dashboard() {
const [projects, setProjects] = useState<Project[]>([]) const [projects, setProjects] = useState<Project[]>([])
@@ -11,6 +14,8 @@ export default function Dashboard() {
const [agents, setAgents] = useState<Agent[]>([]) const [agents, setAgents] = useState<Agent[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<'projects' | 'tasks' | 'agents'>('projects') const [activeTab, setActiveTab] = useState<'projects' | 'tasks' | 'agents'>('projects')
const [showProjectModal, setShowProjectModal] = useState(false)
const [showTaskModal, setShowTaskModal] = useState(false)
useEffect(() => { useEffect(() => {
loadData() loadData()
@@ -47,10 +52,28 @@ export default function Dashboard() {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8"> <div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">AiWorker Dashboard</h1> <h1 className="text-3xl font-bold text-gray-900">AiWorker Dashboard</h1>
<p className="mt-2 text-gray-600">Manage your AI agents and development tasks</p> <p className="mt-2 text-gray-600">Manage your AI agents and development tasks</p>
</div> </div>
<div className="flex space-x-3">
<button
onClick={() => setShowProjectModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
>
New Project
</button>
<button
onClick={() => setShowTaskModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
>
New Task
</button>
</div>
</div>
<StatsCards projects={projects} tasks={tasks} agents={agents} />
{/* Tabs */} {/* Tabs */}
<div className="border-b border-gray-200 mb-6"> <div className="border-b border-gray-200 mb-6">
@@ -95,6 +118,19 @@ export default function Dashboard() {
{activeTab === 'agents' && <AgentStatus agents={agents} onRefresh={loadData} />} {activeTab === 'agents' && <AgentStatus agents={agents} onRefresh={loadData} />}
</div> </div>
</div> </div>
{/* Modals */}
<CreateProjectModal
isOpen={showProjectModal}
onClose={() => setShowProjectModal(false)}
onCreated={loadData}
/>
<CreateTaskModal
isOpen={showTaskModal}
onClose={() => setShowTaskModal(false)}
onCreated={loadData}
projects={projects}
/>
</div> </div>
) )
} }