diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 63e801c..0deb162 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -5,8 +5,8 @@ JIRA AI Fixer - - + +
diff --git a/frontend/src/pages/Integrations.jsx b/frontend/src/pages/Integrations.jsx index f767ab0..ef2f053 100644 --- a/frontend/src/pages/Integrations.jsx +++ b/frontend/src/pages/Integrations.jsx @@ -1,225 +1,126 @@ -import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useAuth } from '../context/AuthContext'; import { integrations } from '../services/api'; -import clsx from 'clsx'; +import { cn } from '../lib/utils'; +import { useState } from 'react'; +import { Plus, Plug, CheckCircle2, XCircle, ExternalLink, Trash2, TestTube, Loader2, AlertCircle, Settings } from 'lucide-react'; -const integrationTypes = [ - { type: 'jira_cloud', name: 'JIRA Cloud', icon: '🔵', desc: 'Atlassian JIRA Cloud' }, - { type: 'servicenow', name: 'ServiceNow', icon: '⚙️', desc: 'ServiceNow ITSM' }, - { type: 'zendesk', name: 'Zendesk', icon: '💚', desc: 'Zendesk Support' }, - { type: 'github', name: 'GitHub', icon: '🐙', desc: 'GitHub Issues' }, - { type: 'gitlab', name: 'GitLab', icon: '🦊', desc: 'GitLab Issues' }, - { type: 'azure_devops', name: 'Azure DevOps', icon: '🔷', desc: 'Azure Boards' }, - { type: 'tickethub', name: 'TicketHub', icon: '🎫', desc: 'TicketHub' }, - { type: 'custom_webhook', name: 'Custom Webhook', icon: '🔗', desc: 'Custom integration' } -]; +const platformConfig = { + jira_cloud: { name: 'JIRA Cloud', color: 'from-blue-600 to-blue-700', icon: '🔵', desc: 'Atlassian JIRA Cloud integration' }, + servicenow: { name: 'ServiceNow', color: 'from-emerald-600 to-emerald-700', icon: '⚙️', desc: 'ServiceNow ITSM platform' }, + github: { name: 'GitHub', color: 'from-gray-700 to-gray-800', icon: '🐙', desc: 'GitHub issues and repositories' }, + gitlab: { name: 'GitLab', color: 'from-orange-600 to-orange-700', icon: '🦊', desc: 'GitLab issues and merge requests' }, + zendesk: { name: 'Zendesk', color: 'from-green-600 to-green-700', icon: '💚', desc: 'Zendesk support tickets' }, + slack: { name: 'Slack', color: 'from-purple-600 to-purple-700', icon: '💬', desc: 'Slack notifications and alerts' }, +}; export default function Integrations() { const { currentOrg } = useAuth(); const queryClient = useQueryClient(); - const [showModal, setShowModal] = useState(false); - const [selectedType, setSelectedType] = useState(null); - const [form, setForm] = useState({}); - + const [showAdd, setShowAdd] = useState(false); + const { data, isLoading } = useQuery({ queryKey: ['integrations', currentOrg?.id], queryFn: () => integrations.list(currentOrg.id), enabled: !!currentOrg }); - - const createMutation = useMutation({ - mutationFn: (data) => integrations.create(currentOrg.id, data), - onSuccess: () => { - queryClient.invalidateQueries(['integrations', currentOrg?.id]); - setShowModal(false); - setForm({}); - setSelectedType(null); - } + + const testMutation = useMutation({ + mutationFn: (id) => integrations.test(currentOrg.id, id), }); - + const deleteMutation = useMutation({ mutationFn: (id) => integrations.delete(currentOrg.id, id), - onSuccess: () => queryClient.invalidateQueries(['integrations', currentOrg?.id]) + onSuccess: () => queryClient.invalidateQueries(['integrations']) }); - - if (!currentOrg) return
Select an organization
; - - const list = data?.data || []; - - const handleCreate = () => { - createMutation.mutate({ - name: form.name, - type: selectedType.type, - base_url: form.base_url, - api_key: form.api_key, - callback_url: form.callback_url - }); - }; - + + if (!currentOrg) return

Select an organization

; + + const activeIntegrations = data?.data || []; + return ( -
-
-

Integrations

-
- - {/* Existing integrations */} -
- {list.map(int => { - const typeInfo = integrationTypes.find(t => t.type === int.type); - return ( -
-
-
- {typeInfo?.icon || '🔗'} -
-

{int.name}

-

{typeInfo?.name}

+ + {/* Active integrations */} + {activeIntegrations.length > 0 && ( +
+

Active Connections

+
+ {activeIntegrations.map(intg => { + const cfg = platformConfig[intg.platform] || { name: intg.platform, color: 'from-gray-600 to-gray-700', icon: '🔌' }; + return ( +
+
+
+
+
+ {cfg.icon} +
+

{intg.name || cfg.name}

+

{cfg.name}

+
+
+ + {intg.is_active ? <> Active : <> Inactive} + +
+ {intg.base_url && ( +

{intg.base_url}

+ )} +
+ + + +
- - {int.status} - -
- -
-
- Issues Processed - {int.issues_processed || 0} -
- {int.last_sync_at && ( -
- Last Event - {new Date(int.last_sync_at).toLocaleDateString()} -
- )} -
- -
-

Webhook URL

- {int.webhook_url} -
- -
- - -
-
- ); - })} - - {list.length === 0 && !isLoading && ( -
- 🔌 -

No integrations yet

-

Connect your first issue tracker to get started

-
- )} -
- - {/* Add integration modal */} - {showModal && ( -
-
-
-

- {selectedType ? `Configure ${selectedType.name}` : 'Add Integration'} -

- -
- -
- {!selectedType ? ( -
- {integrationTypes.map(type => ( - - ))} -
- ) : ( -
-
- - setForm({...form, name: e.target.value})} - placeholder={`My ${selectedType.name}`} - className="input" - /> -
- -
- - setForm({...form, base_url: e.target.value})} - placeholder="https://your-instance.atlassian.net" - className="input" - /> -
- -
- - setForm({...form, api_key: e.target.value})} - placeholder="Your API key" - className="input" - /> -
- -
- - setForm({...form, callback_url: e.target.value})} - placeholder="https://your-instance.atlassian.net/rest/api/2" - className="input" - /> -
- -
- - -
-
- )} -
+ ); + })}
)} + + {/* Available integrations */} +
+

Available Platforms

+
+ {Object.entries(platformConfig).map(([key, cfg]) => { + const isConnected = activeIntegrations.some(i => i.platform === key); + return ( +
+
+
+ {cfg.icon} +
+
+

{cfg.name}

+

{cfg.desc}

+
+
+ +
+ ); + })} +
+
); } diff --git a/frontend/src/pages/IssueDetail.jsx b/frontend/src/pages/IssueDetail.jsx index 5f1f541..4477a7f 100644 --- a/frontend/src/pages/IssueDetail.jsx +++ b/frontend/src/pages/IssueDetail.jsx @@ -1,182 +1,409 @@ +import { useState } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useAuth } from '../context/AuthContext'; import { issues } from '../services/api'; -import clsx from 'clsx'; +import { cn } from '../lib/utils'; +import { + ArrowLeft, ExternalLink, RefreshCw, Clock, CheckCircle2, GitPullRequest, + AlertCircle, FileCode, Lightbulb, Target, Tag, Send, Loader2, + Brain, Code2, FolderTree, Calendar, ChevronRight, Copy, Check, + MessageSquare, XCircle +} from 'lucide-react'; + +const statusConfig = { + pending: { badge: 'badge-yellow', icon: Clock, label: 'Pending' }, + analyzing: { badge: 'badge-blue', icon: Loader2, label: 'Analyzing' }, + analyzed: { badge: 'badge-green', icon: CheckCircle2, label: 'Analyzed' }, + pr_created: { badge: 'badge-purple', icon: GitPullRequest, label: 'PR Created' }, + completed: { badge: 'badge-gray', icon: CheckCircle2, label: 'Completed' }, + error: { badge: 'badge-red', icon: XCircle, label: 'Error' }, +}; + +const DetailSkeleton = () => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); export default function IssueDetail() { const { id } = useParams(); const { currentOrg } = useAuth(); const queryClient = useQueryClient(); - + const [activeTab, setActiveTab] = useState('analysis'); + const [comment, setComment] = useState(''); + const [copied, setCopied] = useState(false); + const { data, isLoading } = useQuery({ queryKey: ['issue', currentOrg?.id, id], queryFn: () => issues.get(currentOrg.id, id), enabled: !!currentOrg }); - + const reanalyzeMutation = useMutation({ mutationFn: () => issues.reanalyze(currentOrg.id, id), + onSuccess: () => queryClient.invalidateQueries(['issue', currentOrg?.id, id]) + }); + + const commentMutation = useMutation({ + mutationFn: (content) => issues.addComment(currentOrg.id, id, { content }), onSuccess: () => { queryClient.invalidateQueries(['issue', currentOrg?.id, id]); + setComment(''); } }); - + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + if (!currentOrg) return null; - if (isLoading) return
Loading...
; - + if (isLoading) return ; + const issue = data?.data; - if (!issue) return
Issue not found
; - + if (!issue) return ( +
+ +

Issue not found

+ ← Back to Issues +
+ ); + + const statusCfg = statusConfig[issue.status] || statusConfig.pending; + const StatusIcon = statusCfg.icon; + const confidencePercent = issue.confidence ? (issue.confidence * 100).toFixed(0) : null; + const confidenceColor = issue.confidence > 0.8 ? 'text-emerald-400' : issue.confidence > 0.5 ? 'text-amber-400' : 'text-red-400'; + + const tabs = [ + { id: 'analysis', label: 'Analysis', icon: Brain }, + { id: 'code', label: 'Suggested Fix', icon: Code2 }, + { id: 'comments', label: 'Comments', icon: MessageSquare }, + ]; + return ( -
-
- ← Back to Issues -
- +
+ {/* Back link */} + + + Back to Issues + + + {/* Header */}
- {issue.external_key || `#${issue.id}`} - - {issue.status} + + {issue.external_key || `#${issue.id}`} + + + {statusCfg.label} + + {issue.priority && ( + + {issue.priority} + + )} +
+

{issue.title}

+
+ {new Date(issue.created_at).toLocaleDateString()} + Source: {issue.source?.replace('_', ' ')}
-

{issue.title}

-

- Source: {issue.source} • Created: {new Date(issue.created_at).toLocaleString()} -

{issue.external_url && ( - - View Original → + + + Original )} -
- -
+ +
{/* Main content */} -
+
{/* Description */}
-

Description

-
-                            {issue.description || 'No description'}
-                        
+
+

+ + Description +

+
+
+
+                                {issue.description || 'No description provided.'}
+                            
+
- - {/* Analysis */} - {issue.root_cause && ( -
-

🔍 Root Cause Analysis

-
-                                {issue.root_cause}
-                            
+ + {/* Tabs */} +
+
+ {tabs.map(tab => { + const Icon = tab.icon; + return ( + + ); + })}
- )} - - {/* Affected Files */} - {issue.affected_files?.length > 0 && ( -
-

📁 Affected Files

-
- {issue.affected_files.map(file => ( - - {file} - - ))} -
+ +
+ {activeTab === 'analysis' && ( +
+ {issue.root_cause ? ( + <> +
+

+ + Root Cause Analysis +

+
+                                                    {issue.root_cause}
+                                                
+
+ {issue.affected_files?.length > 0 && ( +
+

+ + Affected Files +

+
+ {issue.affected_files.map(file => ( + + {file} + + ))} +
+
+ )} + + ) : ( +
+ +

No analysis available yet

+

Click "Re-analyze" to start AI analysis

+
+ )} +
+ )} + + {activeTab === 'code' && ( +
+ {issue.suggested_fix ? ( +
+ +
+                                                {issue.suggested_fix}
+                                            
+
+ ) : ( +
+ +

No suggested fix available

+
+ )} +
+ )} + + {activeTab === 'comments' && ( +
+ {issue.comments?.length > 0 ? ( + issue.comments.map((c, i) => ( +
+
+ {c.author?.[0]?.toUpperCase() || '?'} +
+
+
+ {c.author || 'System'} + {new Date(c.created_at).toLocaleString()} +
+

{c.content}

+
+
+ )) + ) : ( +

No comments yet

+ )} + + {/* Add comment */} +
+ setComment(e.target.value)} + placeholder="Add a comment..." + className="input flex-1" + onKeyDown={e => e.key === 'Enter' && comment.trim() && commentMutation.mutate(comment)} + /> + +
+
+ )}
- )} - - {/* Suggested Fix */} - {issue.suggested_fix && ( -
-

🔧 Suggested Fix

-
-                                {issue.suggested_fix}
-                            
-
- )} +
- - {/* Sidebar */} -
+ + {/* Right sidebar */} +
{/* Confidence */} - {issue.confidence && ( -
-

Confidence

-
-
- {(issue.confidence * 100).toFixed(0)}% -
-
-
+
+ + + + +
+ {confidencePercent}%
+

AI Confidence

)} - + {/* PR Info */} {issue.pr_url && ( -
-

🔀 Pull Request

-

Branch: {issue.pr_branch}

- - View PR → - +
+
+

+ + Pull Request +

+
+
+ {issue.pr_branch && ( +
+

Branch

+ {issue.pr_branch} +
+ )} + + + View Pull Request + +
)} - + {/* Labels */} {issue.labels?.length > 0 && (
-

Labels

-
- {issue.labels.map(label => ( - - {label} - - ))} +
+

+ + Labels +

+
+
+
+ {issue.labels.map(label => ( + {label} + ))} +
)} - + {/* Timeline */}
-

Timeline

-
-
- Created - {new Date(issue.created_at).toLocaleString()} +
+

+ + Timeline +

+
+
+
+
+
+

Created

+

{new Date(issue.created_at).toLocaleString()}

+
{issue.analysis_completed_at && ( -
- Analyzed - {new Date(issue.analysis_completed_at).toLocaleString()} +
+
+
+

Analyzed

+

{new Date(issue.analysis_completed_at).toLocaleString()}

+
+
+ )} + {issue.pr_url && ( +
+
+
+

PR Created

+

Pull request generated

+
)}
diff --git a/frontend/src/pages/Reports.jsx b/frontend/src/pages/Reports.jsx index e218b35..206e0bb 100644 --- a/frontend/src/pages/Reports.jsx +++ b/frontend/src/pages/Reports.jsx @@ -1,106 +1,160 @@ -import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useAuth } from '../context/AuthContext'; import { reports } from '../services/api'; -import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; +import { cn } from '../lib/utils'; +import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'; +import { Download, Calendar, TrendingUp, Clock, CheckCircle2, AlertTriangle, BarChart3, Loader2 } from 'lucide-react'; +import { useState } from 'react'; + +const CustomTooltip = ({ active, payload, label }) => { + if (!active || !payload) return null; + return ( +
+

{label}

+ {payload.map((item, i) => ( +

+ + {item.name}: {item.value} +

+ ))} +
+ ); +}; export default function Reports() { const { currentOrg } = useAuth(); const [days, setDays] = useState(30); - + const { data, isLoading } = useQuery({ - queryKey: ['report', currentOrg?.id, days], + queryKey: ['report-summary', currentOrg?.id, days], queryFn: () => reports.summary(currentOrg.id, days), enabled: !!currentOrg }); - + const handleExport = async () => { - const res = await reports.exportCsv(currentOrg.id, days); - const url = window.URL.createObjectURL(new Blob([res.data])); - const a = document.createElement('a'); - a.href = url; - a.download = `issues-report-${new Date().toISOString().split('T')[0]}.csv`; - a.click(); + try { + const response = await reports.exportCsv(currentOrg.id, days); + const url = URL.createObjectURL(new Blob([response.data])); + const a = document.createElement('a'); + a.href = url; + a.download = `report-${currentOrg.name}-${days}days.csv`; + a.click(); + URL.revokeObjectURL(url); + } catch (err) { console.error(err); } }; - - if (!currentOrg) return
Select an organization
; - + + if (!currentOrg) return

Select an organization

; + const r = data?.data || {}; - + + const summaryCards = [ + { label: 'Total Processed', value: r.total_issues || 0, icon: BarChart3, color: 'text-indigo-400', bg: 'bg-indigo-500/10' }, + { label: 'Success Rate', value: r.success_rate ? `${(r.success_rate * 100).toFixed(0)}%` : 'N/A', icon: CheckCircle2, color: 'text-emerald-400', bg: 'bg-emerald-500/10' }, + { label: 'Avg Resolution', value: r.avg_resolution_hours ? `${r.avg_resolution_hours.toFixed(1)}h` : 'N/A', icon: Clock, color: 'text-amber-400', bg: 'bg-amber-500/10' }, + { label: 'Error Rate', value: r.error_rate ? `${(r.error_rate * 100).toFixed(1)}%` : '0%', icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-500/10' }, + ]; + return ( -
-
-

Reports

-
- - +
+
+
+

Reports & Analytics

+

Performance metrics and insights

+
+
+
+ {[7, 14, 30, 90].map(d => ( + + ))} +
+
- - {/* Summary cards */} -
-
-

Total Issues

-

{r.total_issues || 0}

-
-
-

Analyzed

-

{r.analyzed_issues || 0}

-
-
-

PRs Created

-

{r.prs_created || 0}

-
-
-

Avg Confidence

-

- {r.avg_confidence ? `${(r.avg_confidence * 100).toFixed(0)}%` : 'N/A'} -

-
-
- - {/* Chart */} -
-

Trend

-
- {isLoading ? ( -
Loading...
- ) : ( - - - - - - - - - - - )} -
-
- - {/* Top sources */} -
-

Top Sources

-
- {(r.top_sources || []).map(source => ( -
- {source.source} -
-
+ + {/* Summary */} +
+ {summaryCards.map(stat => { + const Icon = stat.icon; + return ( +
+
+
+

{stat.label}

+

{isLoading ? '—' : stat.value}

+
+
+ +
- {source.count}
- ))} + ); + })} +
+ + {/* Charts */} +
+
+
+

Daily Volume

+
+
+
+ {isLoading ? ( +
+ ) : ( + + + + + + + + + + + + } /> + + + + )} +
+
+
+ +
+
+

Resolution by Source

+
+
+
+ {isLoading ? ( +
+ ) : ( + + ({ + name: name.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()), + value + }))} layout="vertical"> + + + + } /> + + + + )} +
+
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index e836704..608bdc0 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -1,170 +1,235 @@ import { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useAuth } from '../context/AuthContext'; -import { organizations, users } from '../services/api'; +import { cn } from '../lib/utils'; +import { + Settings as SettingsIcon, Building2, Bell, Shield, Key, Globe, + Mail, Save, Loader2, Plus, Trash2, Copy, Eye, EyeOff, Code2 +} from 'lucide-react'; + +const tabs = [ + { id: 'general', label: 'General', icon: Building2 }, + { id: 'notifications', label: 'Notifications', icon: Bell }, + { id: 'security', label: 'Security', icon: Shield }, + { id: 'api', label: 'API Keys', icon: Key }, + { id: 'webhooks', label: 'Webhooks', icon: Globe }, +]; export default function Settings() { - const { currentOrg, selectOrg, user } = useAuth(); - const queryClient = useQueryClient(); - const [activeTab, setActiveTab] = useState('profile'); - const [profileForm, setProfileForm] = useState({ full_name: user?.full_name || '' }); - const [orgForm, setOrgForm] = useState({ name: currentOrg?.name || '', slug: currentOrg?.slug || '' }); - const [newOrgForm, setNewOrgForm] = useState({ name: '', slug: '' }); - - const updateProfileMutation = useMutation({ - mutationFn: (data) => users.updateMe(data), - onSuccess: () => queryClient.invalidateQueries(['user']) - }); - - const updateOrgMutation = useMutation({ - mutationFn: (data) => organizations.update(currentOrg.id, data), - onSuccess: (res) => { - queryClient.invalidateQueries(['organizations']); - selectOrg(res.data); - } - }); - - const createOrgMutation = useMutation({ - mutationFn: (data) => organizations.create(data), - onSuccess: (res) => { - queryClient.invalidateQueries(['organizations']); - selectOrg(res.data); - setNewOrgForm({ name: '', slug: '' }); - } - }); - - const tabs = [ - { id: 'profile', label: 'Profile', icon: '👤' }, - { id: 'organization', label: 'Organization', icon: '🏢' }, - { id: 'new-org', label: 'New Organization', icon: '➕' } - ]; - + const { currentOrg } = useAuth(); + const [activeTab, setActiveTab] = useState('general'); + const [saving, setSaving] = useState(false); + const [showToken, setShowToken] = useState(false); + + if (!currentOrg) return

Select an organization

; + + const handleSave = async () => { + setSaving(true); + await new Promise(r => setTimeout(r, 1000)); + setSaving(false); + }; + return ( -
-

Settings

- -
- {/* Tabs */} -
- {tabs.map(tab => ( - - ))} +
+
+
+

Settings

+

Manage your organization settings

- +
+ +
+ {/* Sidebar tabs */} +
+
+ {tabs.map(tab => { + const Icon = tab.icon; + return ( + + ); + })} +
+
+ {/* Content */} -
- {activeTab === 'profile' && ( -
-

Profile Settings

-
+
+ {activeTab === 'general' && ( +
+
+

Organization Details

+
+
- - + +
- - setProfileForm({...profileForm, full_name: e.target.value})} - className="input" - /> + + +
+
+ + +
+
+
-
)} - - {activeTab === 'organization' && currentOrg && ( -
-

Organization Settings

-
-
- - setOrgForm({...orgForm, name: e.target.value})} - className="input" - /> + + {activeTab === 'notifications' && ( +
+
+

Notification Preferences

+
+
+ {[ + { label: 'New issue received', desc: 'When a new issue arrives from an integration' }, + { label: 'Analysis completed', desc: 'When AI finishes analyzing an issue' }, + { label: 'PR created', desc: 'When a Pull Request is automatically generated' }, + { label: 'Analysis error', desc: 'When AI fails to analyze an issue' }, + { label: 'Daily digest', desc: 'Summary of daily activity' }, + ].map(item => ( +
+
+

{item.label}

+

{item.desc}

+
+
)} - - {activeTab === 'new-org' && ( -
-

Create New Organization

-
-
- - setNewOrgForm({...newOrgForm, name: e.target.value})} - className="input" - placeholder="Acme Corp" - /> + + {activeTab === 'security' && ( +
+
+

Security Settings

+
+
+
+
+

Two-Factor Authentication

+

Require 2FA for all organization members

+
+