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 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user