diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a5c1d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +.env.local +*.log +.DS_Store diff --git a/README.md b/README.md index 6918e7f..7b41db9 100644 --- a/README.md +++ b/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 \ No newline at end of file +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 diff --git a/index.html b/index.html new file mode 100644 index 0000000..b23943d --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + JIRA AI Fixer Portal + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..d556e5f --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..083f157 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,3 @@ + + 🤖 + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..e192f44 --- /dev/null +++ b/src/App.tsx @@ -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 ( + + }> + } /> + } /> + } /> + } /> + } /> + + + ) +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..9cbe88d --- /dev/null +++ b/src/components/Layout.tsx @@ -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 ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ +
+
+ ) +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..ef59836 --- /dev/null +++ b/src/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: 'Inter', system-ui, sans-serif; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..f4da9f2 --- /dev/null +++ b/src/main.tsx @@ -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( + + + + + + + , +) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..94693df --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -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 ( +
+

Dashboard

+ + {/* Stats Grid */} +
+ + + + +
+ + {/* Recent Issues */} +
+
+

Recent Issues

+ + View all → + +
+
+ {recentIssues.length === 0 ? ( +
No issues yet
+ ) : ( + recentIssues.map(issue => ( + +
+
+
+ + {issue.external_key || `#${issue.id}`} + + +
+

{issue.title}

+
+ {issue.confidence && ( + + {Math.round(issue.confidence * 100)}% + + )} +
+ + )) + )} +
+
+
+ ) +} + +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 ( +
+
+
+

{title}

+

+ {value} +

+
+
+ {icon} +
+
+
+ ) +} + +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 ( + + {status} + + ) +} diff --git a/src/pages/IssueDetail.tsx b/src/pages/IssueDetail.tsx new file mode 100644 index 0000000..9ae015d --- /dev/null +++ b/src/pages/IssueDetail.tsx @@ -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 ( +
+
Loading...
+
+ ) + } + + if (error || !issue) { + return ( +
+
Issue not found
+ + ← Back to issues + +
+ ) + } + + 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 ( +
+ {/* Header */} +
+ + ← Back to issues + +
+ +
+
+
+
+ + {issue.external_key || `#${issue.id}`} + + + {issue.status} + +
+

{issue.title}

+

Source: {issue.source}

+
+ + {issue.confidence && ( +
+

Confidence

+

+ {Math.round(issue.confidence * 100)}% +

+
+ )} +
+
+ + {/* Description */} +
+

Description

+
+          {issue.description}
+        
+
+ + {/* Analysis */} + {issue.analysis && ( +
+

🔍 Analysis

+
+            {issue.analysis}
+          
+
+ )} + + {/* Affected Files */} + {affectedFiles.length > 0 && ( +
+

📁 Affected Files

+
+ {affectedFiles.map((file, i) => ( + + {file} + + ))} +
+
+ )} + + {/* Suggested Fix */} + {issue.suggested_fix && ( +
+

🔧 Suggested Fix

+
+            {issue.suggested_fix}
+          
+
+ )} + + {/* Metadata */} +
+

Details

+
+
+ Created: + {new Date(issue.created_at).toLocaleString()} +
+ {issue.analyzed_at && ( +
+ Analyzed: + {new Date(issue.analyzed_at).toLocaleString()} +
+ )} +
+
+
+ ) +} diff --git a/src/pages/Issues.tsx b/src/pages/Issues.tsx new file mode 100644 index 0000000..b2d2e39 --- /dev/null +++ b/src/pages/Issues.tsx @@ -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 ( +
+
+

Issues

+ +
+ +
+ {isLoading ? ( +
Loading...
+ ) : !issues?.length ? ( +
No issues found
+ ) : ( +
+ {issues.map(issue => ( + + ))} +
+ )} +
+
+ ) +} + +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 ( + +
+
+
+ + {issue.external_key || `#${issue.id}`} + + + {issue.status} + + {issue.source} +
+

{issue.title}

+

{issue.description}

+
+ {issue.confidence && ( +
+
+
+
+ + {Math.round(issue.confidence * 100)}% + +
+ )} +
+ + ) +} diff --git a/src/pages/Repositories.tsx b/src/pages/Repositories.tsx new file mode 100644 index 0000000..aaf8206 --- /dev/null +++ b/src/pages/Repositories.tsx @@ -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 ( +
+
+

Repositories

+ +
+ +
+ {repos.map((repo, i) => ( +
+
+
+ 📁 +
+
+

{repo.name}

+

{repo.url}

+
+
+
+
+

{repo.files}

+

Files

+
+
+

{repo.language}

+

Language

+
+ + {repo.status} + +
+
+ ))} +
+ + {/* Integrations */} +

Integrations

+
+ + + + + +
+
+ ) +} + +function IntegrationCard({ name, icon, status, description }: { + name: string + icon: string + status: 'connected' | 'available' + description: string +}) { + return ( +
+
+
+ {icon} +

{name}

+
+ + {status} + +
+

{description}

+ {status === 'available' && ( + + )} +
+ ) +} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx new file mode 100644 index 0000000..3089da0 --- /dev/null +++ b/src/pages/Settings.tsx @@ -0,0 +1,119 @@ +export default function Settings() { + return ( +
+

Settings

+ + {/* Webhook Endpoints */} +
+

Webhook Endpoints

+
+ + + +
+
+ + {/* Issue Trackers */} +
+

Issue Tracker Integrations

+
+ + + + +
+
+ + {/* AI Settings */} +
+

AI Configuration

+
+
+ + +
+
+ + +
+
+ + +
+ 0% + 70% + 100% +
+
+
+
+
+ ) +} + +function EndpointRow({ method, path, description }: { + method: string + path: string + description: string +}) { + return ( +
+ + {method} + + {path} + {description} + +
+ ) +} + +function TrackerCard({ name, icon, status }: { + name: string + icon: string + status: 'active' | 'ready' +}) { + return ( +
+
+ {icon} + {name} +
+ + {status} + +
+ ) +} diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..c101cf2 --- /dev/null +++ b/src/services/api.ts @@ -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 => { + const params = status ? { status } : {} + const { data } = await api.get('/issues', { params }) + return data + }, + + get: async (id: number): Promise => { + const { data } = await api.get(`/issues/${id}`) + return data + }, + + getStats: async (): Promise => { + 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 + } + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..7141e45 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3934b8f --- /dev/null +++ b/tsconfig.json @@ -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" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6318ceb --- /dev/null +++ b/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' + } + } +})