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