Initial commit: JIRA AI Fixer Portal
- React 18 + TypeScript + Vite - TailwindCSS styling - React Query for data fetching - React Router for navigation Pages: - Dashboard with stats - Issues list with filters - Issue detail view - Repositories management - Settings/Integrations
This commit is contained in:
parent
0b24d51ce1
commit
fd966983a3
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
56
README.md
56
README.md
|
|
@ -1,3 +1,55 @@
|
||||||
# jira-ai-fixer-portal
|
# JIRA AI Fixer Portal
|
||||||
|
|
||||||
JIRA AI Fixer Portal - React Dashboard for intelligent support case resolution
|
React dashboard for the JIRA AI Fixer - Intelligent Support Case Resolution system.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📊 **Dashboard** - Real-time stats and recent issues
|
||||||
|
- 🎫 **Issues** - Browse and filter analyzed issues
|
||||||
|
- 📁 **Repositories** - Manage connected code repositories
|
||||||
|
- ⚙️ **Settings** - Configure integrations and AI settings
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React 18 + TypeScript
|
||||||
|
- Vite
|
||||||
|
- TailwindCSS
|
||||||
|
- React Query
|
||||||
|
- React Router
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start dev server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_URL=https://jira-fixer.startdata.com.br/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
### Issue Trackers
|
||||||
|
- TicketHub (Active)
|
||||||
|
- JIRA (Ready)
|
||||||
|
- ServiceNow (Ready)
|
||||||
|
- Azure DevOps (Ready)
|
||||||
|
|
||||||
|
### Code Repositories
|
||||||
|
- Gitea (Active)
|
||||||
|
- GitHub (Ready)
|
||||||
|
- GitLab (Ready)
|
||||||
|
- Bitbucket (Ready)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>JIRA AI Fixer Portal</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "jira-ai-fixer-portal",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "JIRA AI Fixer Portal - React Dashboard",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.22.0",
|
||||||
|
"@tanstack/react-query": "^5.17.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"date-fns": "^3.3.0",
|
||||||
|
"recharts": "^2.12.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.48",
|
||||||
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"postcss": "^8.4.33",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<text y=".9em" font-size="90">🤖</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 114 B |
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Routes, Route } from 'react-router-dom'
|
||||||
|
import Layout from './components/Layout'
|
||||||
|
import Dashboard from './pages/Dashboard'
|
||||||
|
import Issues from './pages/Issues'
|
||||||
|
import IssueDetail from './pages/IssueDetail'
|
||||||
|
import Repositories from './pages/Repositories'
|
||||||
|
import Settings from './pages/Settings'
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Layout />}>
|
||||||
|
<Route index element={<Dashboard />} />
|
||||||
|
<Route path="issues" element={<Issues />} />
|
||||||
|
<Route path="issues/:id" element={<IssueDetail />} />
|
||||||
|
<Route path="repositories" element={<Repositories />} />
|
||||||
|
<Route path="settings" element={<Settings />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Outlet, NavLink } from 'react-router-dom'
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ to: '/', label: 'Dashboard', icon: '📊' },
|
||||||
|
{ to: '/issues', label: 'Issues', icon: '🎫' },
|
||||||
|
{ to: '/repositories', label: 'Repositories', icon: '📁' },
|
||||||
|
{ to: '/settings', label: 'Settings', icon: '⚙️' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white flex">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
|
||||||
|
<div className="p-4 border-b border-gray-700">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl">🤖</span>
|
||||||
|
<div>
|
||||||
|
<h1 className="font-bold">JIRA AI Fixer</h1>
|
||||||
|
<p className="text-xs text-gray-400">Portal</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 p-4">
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{navItems.map(item => (
|
||||||
|
<li key={item.to}>
|
||||||
|
<NavLink
|
||||||
|
to={item.to}
|
||||||
|
end={item.to === '/'}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-400 hover:bg-gray-700 hover:text-white'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{item.icon}</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-gray-700 text-xs text-gray-500">
|
||||||
|
<p>JIRA AI Fixer v1.0.0</p>
|
||||||
|
<p className="mt-1">© 2026 StartData</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main className="flex-1 overflow-auto">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { issuesApi } from '../services/api'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const { data: stats } = useQuery({
|
||||||
|
queryKey: ['stats'],
|
||||||
|
queryFn: issuesApi.getStats,
|
||||||
|
refetchInterval: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: issues } = useQuery({
|
||||||
|
queryKey: ['issues'],
|
||||||
|
queryFn: () => issuesApi.list(),
|
||||||
|
refetchInterval: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const recentIssues = issues?.slice(0, 5) || []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
|
<StatCard
|
||||||
|
title="Total Issues"
|
||||||
|
value={stats?.total || 0}
|
||||||
|
icon="📋"
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Analyzed"
|
||||||
|
value={stats?.analyzed || 0}
|
||||||
|
icon="✅"
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="PRs Created"
|
||||||
|
value={stats?.prs_created || 0}
|
||||||
|
icon="🔀"
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Avg Confidence"
|
||||||
|
value={`${stats?.avg_confidence || 0}%`}
|
||||||
|
icon="🎯"
|
||||||
|
color="yellow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Issues */}
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700">
|
||||||
|
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||||
|
<h2 className="font-semibold">Recent Issues</h2>
|
||||||
|
<Link to="/issues" className="text-sm text-blue-400 hover:text-blue-300">
|
||||||
|
View all →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-700">
|
||||||
|
{recentIssues.length === 0 ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">No issues yet</div>
|
||||||
|
) : (
|
||||||
|
recentIssues.map(issue => (
|
||||||
|
<Link
|
||||||
|
key={issue.id}
|
||||||
|
to={`/issues/${issue.id}`}
|
||||||
|
className="p-4 block hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-blue-400 text-sm">
|
||||||
|
{issue.external_key || `#${issue.id}`}
|
||||||
|
</span>
|
||||||
|
<StatusBadge status={issue.status} />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-medium mt-1">{issue.title}</h3>
|
||||||
|
</div>
|
||||||
|
{issue.confidence && (
|
||||||
|
<span className="text-sm text-green-400">
|
||||||
|
{Math.round(issue.confidence * 100)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ title, value, icon, color }: {
|
||||||
|
title: string
|
||||||
|
value: number | string
|
||||||
|
icon: string
|
||||||
|
color: 'blue' | 'green' | 'purple' | 'yellow'
|
||||||
|
}) {
|
||||||
|
const colors = {
|
||||||
|
blue: 'bg-blue-500/20 text-blue-400',
|
||||||
|
green: 'bg-green-500/20 text-green-400',
|
||||||
|
purple: 'bg-purple-500/20 text-purple-400',
|
||||||
|
yellow: 'bg-yellow-500/20 text-yellow-400',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-sm">{title}</p>
|
||||||
|
<p className={`text-3xl font-bold mt-1 ${colors[color].split(' ')[1]}`}>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${colors[color].split(' ')[0]}`}>
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ status }: { status: string }) {
|
||||||
|
const styles = {
|
||||||
|
analyzed: 'bg-green-500/20 text-green-400',
|
||||||
|
pending: 'bg-yellow-500/20 text-yellow-400',
|
||||||
|
error: 'bg-red-500/20 text-red-400',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs ${styles[status as keyof typeof styles] || 'bg-gray-500/20 text-gray-400'}`}>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { useParams, Link } from 'react-router-dom'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { issuesApi } from '../services/api'
|
||||||
|
|
||||||
|
export default function IssueDetail() {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const { data: issue, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['issue', id],
|
||||||
|
queryFn: () => issuesApi.get(Number(id)),
|
||||||
|
enabled: !!id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="text-center text-gray-500">Loading...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !issue) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="text-center text-red-400">Issue not found</div>
|
||||||
|
<Link to="/issues" className="block text-center text-blue-400 mt-4">
|
||||||
|
← Back to issues
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusStyles = {
|
||||||
|
analyzed: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||||
|
pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
|
||||||
|
error: 'bg-red-500/20 text-red-400 border-red-500/30',
|
||||||
|
}
|
||||||
|
|
||||||
|
let affectedFiles: string[] = []
|
||||||
|
try {
|
||||||
|
affectedFiles = JSON.parse(issue.affected_files || '[]')
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Link to="/issues" className="text-blue-400 hover:text-blue-300 text-sm">
|
||||||
|
← Back to issues
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="font-mono text-blue-400 text-lg">
|
||||||
|
{issue.external_key || `#${issue.id}`}
|
||||||
|
</span>
|
||||||
|
<span className={`px-2 py-1 rounded text-sm border ${statusStyles[issue.status] || ''}`}>
|
||||||
|
{issue.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold mt-2">{issue.title}</h1>
|
||||||
|
<p className="text-gray-400 mt-1">Source: {issue.source}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{issue.confidence && (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm text-gray-400">Confidence</p>
|
||||||
|
<p className="text-3xl font-bold text-green-400">
|
||||||
|
{Math.round(issue.confidence * 100)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
|
||||||
|
<h2 className="font-semibold mb-3">Description</h2>
|
||||||
|
<pre className="whitespace-pre-wrap text-gray-300 bg-gray-900 p-4 rounded-lg">
|
||||||
|
{issue.description}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Analysis */}
|
||||||
|
{issue.analysis && (
|
||||||
|
<div className="bg-green-500/10 border border-green-500/30 rounded-xl p-6 mb-6">
|
||||||
|
<h2 className="font-semibold text-green-400 mb-3">🔍 Analysis</h2>
|
||||||
|
<pre className="whitespace-pre-wrap text-gray-300">
|
||||||
|
{issue.analysis}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Affected Files */}
|
||||||
|
{affectedFiles.length > 0 && (
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
|
||||||
|
<h2 className="font-semibold mb-3">📁 Affected Files</h2>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{affectedFiles.map((file, i) => (
|
||||||
|
<span key={i} className="px-3 py-1 bg-gray-700 rounded-lg font-mono text-sm">
|
||||||
|
{file}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Suggested Fix */}
|
||||||
|
{issue.suggested_fix && (
|
||||||
|
<div className="bg-purple-500/10 border border-purple-500/30 rounded-xl p-6 mb-6">
|
||||||
|
<h2 className="font-semibold text-purple-400 mb-3">🔧 Suggested Fix</h2>
|
||||||
|
<pre className="whitespace-pre-wrap font-mono text-sm bg-gray-900 p-4 rounded-lg">
|
||||||
|
{issue.suggested_fix}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6">
|
||||||
|
<h2 className="font-semibold mb-3">Details</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400">Created:</span>
|
||||||
|
<span className="ml-2">{new Date(issue.created_at).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
{issue.analyzed_at && (
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-400">Analyzed:</span>
|
||||||
|
<span className="ml-2">{new Date(issue.analyzed_at).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { issuesApi, Issue } from '../services/api'
|
||||||
|
|
||||||
|
export default function Issues() {
|
||||||
|
const [filter, setFilter] = useState('')
|
||||||
|
|
||||||
|
const { data: issues, isLoading } = useQuery({
|
||||||
|
queryKey: ['issues', filter],
|
||||||
|
queryFn: () => issuesApi.list(filter || undefined),
|
||||||
|
refetchInterval: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold">Issues</h1>
|
||||||
|
<select
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
className="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"
|
||||||
|
>
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="analyzed">Analyzed</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">Loading...</div>
|
||||||
|
) : !issues?.length ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">No issues found</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-gray-700">
|
||||||
|
{issues.map(issue => (
|
||||||
|
<IssueRow key={issue.id} issue={issue} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function IssueRow({ issue }: { issue: Issue }) {
|
||||||
|
const statusStyles = {
|
||||||
|
analyzed: 'bg-green-500/20 text-green-400',
|
||||||
|
pending: 'bg-yellow-500/20 text-yellow-400',
|
||||||
|
error: 'bg-red-500/20 text-red-400',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/issues/${issue.id}`}
|
||||||
|
className="p-4 block hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-blue-400 text-sm">
|
||||||
|
{issue.external_key || `#${issue.id}`}
|
||||||
|
</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs ${statusStyles[issue.status] || 'bg-gray-500/20 text-gray-400'}`}>
|
||||||
|
{issue.status}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">{issue.source}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-medium mt-1">{issue.title}</h3>
|
||||||
|
<p className="text-sm text-gray-400 mt-1 line-clamp-2">{issue.description}</p>
|
||||||
|
</div>
|
||||||
|
{issue.confidence && (
|
||||||
|
<div className="ml-4 text-right">
|
||||||
|
<div className="w-24 bg-gray-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-green-500 h-2 rounded-full"
|
||||||
|
style={{ width: `${issue.confidence * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400 mt-1">
|
||||||
|
{Math.round(issue.confidence * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
export default function Repositories() {
|
||||||
|
const repos = [
|
||||||
|
{
|
||||||
|
name: 'cobol-sample-app',
|
||||||
|
url: 'https://gitea.startdata.com.br/startdata/cobol-sample-app',
|
||||||
|
files: 4,
|
||||||
|
language: 'COBOL',
|
||||||
|
status: 'indexed',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold">Repositories</h1>
|
||||||
|
<button className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
|
||||||
|
<span>+</span>
|
||||||
|
<span>Add Repository</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700">
|
||||||
|
{repos.map((repo, i) => (
|
||||||
|
<div key={i} className="p-4 flex items-center justify-between border-b border-gray-700 last:border-0">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-gray-700 rounded-lg flex items-center justify-center">
|
||||||
|
<span className="text-2xl">📁</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">{repo.name}</h3>
|
||||||
|
<p className="text-sm text-gray-400">{repo.url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-lg font-semibold">{repo.files}</p>
|
||||||
|
<p className="text-xs text-gray-400">Files</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-lg font-semibold">{repo.language}</p>
|
||||||
|
<p className="text-xs text-gray-400">Language</p>
|
||||||
|
</div>
|
||||||
|
<span className="px-3 py-1 bg-green-500/20 text-green-400 rounded-full text-sm">
|
||||||
|
{repo.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Integrations */}
|
||||||
|
<h2 className="text-xl font-bold mt-8 mb-4">Integrations</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<IntegrationCard
|
||||||
|
name="Gitea"
|
||||||
|
icon="📦"
|
||||||
|
status="connected"
|
||||||
|
description="Self-hosted Git service"
|
||||||
|
/>
|
||||||
|
<IntegrationCard
|
||||||
|
name="GitHub"
|
||||||
|
icon="🐙"
|
||||||
|
status="available"
|
||||||
|
description="GitHub repositories"
|
||||||
|
/>
|
||||||
|
<IntegrationCard
|
||||||
|
name="GitLab"
|
||||||
|
icon="🦊"
|
||||||
|
status="available"
|
||||||
|
description="GitLab repositories"
|
||||||
|
/>
|
||||||
|
<IntegrationCard
|
||||||
|
name="Bitbucket"
|
||||||
|
icon="🪣"
|
||||||
|
status="available"
|
||||||
|
description="Atlassian Bitbucket"
|
||||||
|
/>
|
||||||
|
<IntegrationCard
|
||||||
|
name="Azure DevOps"
|
||||||
|
icon="🔷"
|
||||||
|
status="available"
|
||||||
|
description="Azure Repos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function IntegrationCard({ name, icon, status, description }: {
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
status: 'connected' | 'available'
|
||||||
|
description: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
|
<h3 className="font-semibold">{name}</h3>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs ${
|
||||||
|
status === 'connected'
|
||||||
|
? 'bg-green-500/20 text-green-400'
|
||||||
|
: 'bg-gray-500/20 text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400">{description}</p>
|
||||||
|
{status === 'available' && (
|
||||||
|
<button className="mt-3 text-sm text-blue-400 hover:text-blue-300">
|
||||||
|
Connect →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
export default function Settings() {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Settings</h1>
|
||||||
|
|
||||||
|
{/* Webhook Endpoints */}
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
|
||||||
|
<h2 className="font-semibold mb-4">Webhook Endpoints</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<EndpointRow
|
||||||
|
method="POST"
|
||||||
|
path="/api/webhook/tickethub"
|
||||||
|
description="Receive ticket events from TicketHub"
|
||||||
|
/>
|
||||||
|
<EndpointRow
|
||||||
|
method="POST"
|
||||||
|
path="/api/webhook/jira"
|
||||||
|
description="Receive issue events from JIRA"
|
||||||
|
/>
|
||||||
|
<EndpointRow
|
||||||
|
method="POST"
|
||||||
|
path="/api/webhook/servicenow"
|
||||||
|
description="Receive incident events from ServiceNow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Issue Trackers */}
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
|
||||||
|
<h2 className="font-semibold mb-4">Issue Tracker Integrations</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<TrackerCard name="TicketHub" icon="🎫" status="active" />
|
||||||
|
<TrackerCard name="JIRA" icon="🔵" status="ready" />
|
||||||
|
<TrackerCard name="ServiceNow" icon="🟢" status="ready" />
|
||||||
|
<TrackerCard name="Azure DevOps" icon="🔷" status="ready" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Settings */}
|
||||||
|
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6">
|
||||||
|
<h2 className="font-semibold mb-4">AI Configuration</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-400 mb-1">LLM Provider</label>
|
||||||
|
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2">
|
||||||
|
<option>OpenRouter (Free Tier)</option>
|
||||||
|
<option>OpenAI</option>
|
||||||
|
<option>Anthropic</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-400 mb-1">Model</label>
|
||||||
|
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2">
|
||||||
|
<option>meta-llama/llama-3.3-70b-instruct (Free)</option>
|
||||||
|
<option>gpt-4-turbo</option>
|
||||||
|
<option>claude-3-opus</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-gray-400 mb-1">Confidence Threshold</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
defaultValue="70"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500">
|
||||||
|
<span>0%</span>
|
||||||
|
<span>70%</span>
|
||||||
|
<span>100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EndpointRow({ method, path, description }: {
|
||||||
|
method: string
|
||||||
|
path: string
|
||||||
|
description: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4 p-3 bg-gray-700/50 rounded-lg">
|
||||||
|
<span className="px-2 py-1 bg-blue-500/20 text-blue-400 rounded text-xs font-mono">
|
||||||
|
{method}
|
||||||
|
</span>
|
||||||
|
<code className="flex-1 font-mono text-sm">{path}</code>
|
||||||
|
<span className="text-sm text-gray-400">{description}</span>
|
||||||
|
<button className="text-gray-400 hover:text-white">
|
||||||
|
📋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TrackerCard({ name, icon, status }: {
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
status: 'active' | 'ready'
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between p-4 bg-gray-700/50 rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
|
<span className="font-medium">{name}</span>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs ${
|
||||||
|
status === 'active'
|
||||||
|
? 'bg-green-500/20 text-green-400'
|
||||||
|
: 'bg-gray-500/20 text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || '/api'
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: API_URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface Issue {
|
||||||
|
id: number
|
||||||
|
external_key: string
|
||||||
|
source: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
status: 'pending' | 'analyzed' | 'error'
|
||||||
|
analysis?: string
|
||||||
|
affected_files?: string
|
||||||
|
suggested_fix?: string
|
||||||
|
confidence?: number
|
||||||
|
created_at: string
|
||||||
|
analyzed_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stats {
|
||||||
|
total: number
|
||||||
|
analyzed: number
|
||||||
|
pending: number
|
||||||
|
error: number
|
||||||
|
prs_created: number
|
||||||
|
avg_confidence: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const issuesApi = {
|
||||||
|
list: async (status?: string): Promise<Issue[]> => {
|
||||||
|
const params = status ? { status } : {}
|
||||||
|
const { data } = await api.get('/issues', { params })
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async (id: number): Promise<Issue> => {
|
||||||
|
const { data } = await api.get(`/issues/${id}`)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
getStats: async (): Promise<Stats> => {
|
||||||
|
const issues = await issuesApi.list()
|
||||||
|
const analyzed = issues.filter(i => i.status === 'analyzed')
|
||||||
|
return {
|
||||||
|
total: issues.length,
|
||||||
|
analyzed: analyzed.length,
|
||||||
|
pending: issues.filter(i => i.status === 'pending').length,
|
||||||
|
error: issues.filter(i => i.status === 'error').length,
|
||||||
|
prs_created: analyzed.filter(i => i.suggested_fix).length,
|
||||||
|
avg_confidence: analyzed.length
|
||||||
|
? Math.round(analyzed.reduce((a, i) => a + (i.confidence || 0), 0) / analyzed.length * 100)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:8000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue