Enhance frontend dashboard with stats, modals, and actions
All checks were successful
Build and Push Frontend / build (push) Successful in 16s
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:
108
src/components/CreateProjectModal.tsx
Normal file
108
src/components/CreateProjectModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
127
src/components/CreateTaskModal.tsx
Normal file
127
src/components/CreateTaskModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
71
src/components/StatsCards.tsx
Normal file
71
src/components/StatsCards.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import type { Project, Task, Agent } from '../types'
|
||||
import ProjectList from '../components/ProjectList'
|
||||
import TaskList from '../components/TaskList'
|
||||
import AgentStatus from '../components/AgentStatus'
|
||||
import StatsCards from '../components/StatsCards'
|
||||
import CreateProjectModal from '../components/CreateProjectModal'
|
||||
import CreateTaskModal from '../components/CreateTaskModal'
|
||||
|
||||
export default function Dashboard() {
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
@@ -11,6 +14,8 @@ export default function Dashboard() {
|
||||
const [agents, setAgents] = useState<Agent[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState<'projects' | 'tasks' | 'agents'>('projects')
|
||||
const [showProjectModal, setShowProjectModal] = useState(false)
|
||||
const [showTaskModal, setShowTaskModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
@@ -47,10 +52,28 @@ export default function Dashboard() {
|
||||
return (
|
||||
<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="mb-8">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
</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 */}
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
@@ -95,6 +118,19 @@ export default function Dashboard() {
|
||||
{activeTab === 'agents' && <AgentStatus agents={agents} onRefresh={loadData} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<CreateProjectModal
|
||||
isOpen={showProjectModal}
|
||||
onClose={() => setShowProjectModal(false)}
|
||||
onCreated={loadData}
|
||||
/>
|
||||
<CreateTaskModal
|
||||
isOpen={showTaskModal}
|
||||
onClose={() => setShowTaskModal(false)}
|
||||
onCreated={loadData}
|
||||
projects={projects}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user