feat: Enterprise-grade portal with full functionality

NEW FEATURES:
- Integrations page: Connect JIRA, ServiceNow, GitHub, GitLab, etc.
  - Provider-specific configuration forms
  - Test connection functionality
  - Sync status tracking

- Automation Rules: Configure when/how issues are analyzed
  - Visual rule builder
  - Conditions and actions
  - Enable/disable per rule
  - Statistics per rule

- Analytics Dashboard:
  - KPI cards with trends
  - Issues over time chart
  - Resolution by category
  - Top affected modules
  - Recent activity feed

- Team Management:
  - Invite members
  - Role-based access (Admin/Developer/Viewer)
  - Activity tracking
  - Notification preferences

- Settings:
  - General org settings
  - AI model configuration
  - Notifications (Email, Slack)
  - Security (SSO, 2FA, IP allowlist)
  - API keys management

UI COMPONENTS:
- Card, Button, Input, Select
- Badge, Modal, Tabs
- Consistent dark theme
- Collapsible sidebar

ARCHITECTURE:
- React 18 + TypeScript
- TailwindCSS
- React Query
- React Router
This commit is contained in:
Ricel Leite 2026-02-18 18:31:27 -03:00
parent fd966983a3
commit cc24d75982
15 changed files with 1615 additions and 150 deletions

View File

@ -4,6 +4,10 @@ import Dashboard from './pages/Dashboard'
import Issues from './pages/Issues' import Issues from './pages/Issues'
import IssueDetail from './pages/IssueDetail' import IssueDetail from './pages/IssueDetail'
import Repositories from './pages/Repositories' import Repositories from './pages/Repositories'
import Integrations from './pages/Integrations'
import Rules from './pages/Rules'
import Analytics from './pages/Analytics'
import Team from './pages/Team'
import Settings from './pages/Settings' import Settings from './pages/Settings'
export default function App() { export default function App() {
@ -11,9 +15,13 @@ export default function App() {
<Routes> <Routes>
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="analytics" element={<Analytics />} />
<Route path="issues" element={<Issues />} /> <Route path="issues" element={<Issues />} />
<Route path="issues/:id" element={<IssueDetail />} /> <Route path="issues/:id" element={<IssueDetail />} />
<Route path="repositories" element={<Repositories />} /> <Route path="repositories" element={<Repositories />} />
<Route path="integrations" element={<Integrations />} />
<Route path="rules" element={<Rules />} />
<Route path="team" element={<Team />} />
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
</Route> </Route>
</Routes> </Routes>

View File

@ -1,60 +1,137 @@
import { Outlet, NavLink } from 'react-router-dom' import { Outlet, NavLink, useLocation } from 'react-router-dom'
import { useState } from 'react'
const navItems = [ const navSections = [
{ to: '/', label: 'Dashboard', icon: '📊' }, {
{ to: '/issues', label: 'Issues', icon: '🎫' }, title: 'Overview',
{ to: '/repositories', label: 'Repositories', icon: '📁' }, items: [
{ to: '/settings', label: 'Settings', icon: '⚙️' }, { to: '/', label: 'Dashboard', icon: '📊' },
{ to: '/analytics', label: 'Analytics', icon: '📈' },
],
},
{
title: 'Work',
items: [
{ to: '/issues', label: 'Issues', icon: '🎫' },
{ to: '/repositories', label: 'Repositories', icon: '📁' },
],
},
{
title: 'Configuration',
items: [
{ to: '/integrations', label: 'Integrations', icon: '🔌' },
{ to: '/rules', label: 'Automation', icon: '⚡' },
{ to: '/team', label: 'Team', icon: '👥' },
{ to: '/settings', label: 'Settings', icon: '⚙️' },
],
},
] ]
export default function Layout() { export default function Layout() {
const [collapsed, setCollapsed] = useState(false)
const location = useLocation()
return ( return (
<div className="min-h-screen bg-gray-900 text-white flex"> <div className="min-h-screen bg-gray-900 text-white flex">
{/* Sidebar */} {/* Sidebar */}
<aside className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col"> <aside className={`${collapsed ? 'w-16' : 'w-64'} bg-gray-800 border-r border-gray-700 flex flex-col transition-all duration-200`}>
{/* Logo */}
<div className="p-4 border-b border-gray-700"> <div className="p-4 border-b border-gray-700">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-2xl">🤖</span> <span className="text-2xl">🤖</span>
<div> {!collapsed && (
<h1 className="font-bold">JIRA AI Fixer</h1> <div>
<p className="text-xs text-gray-400">Portal</p> <h1 className="font-bold text-lg">JIRA AI Fixer</h1>
</div> <p className="text-xs text-gray-400">Enterprise</p>
</div>
)}
</div> </div>
</div> </div>
<nav className="flex-1 p-4"> {/* Navigation */}
<ul className="space-y-1"> <nav className="flex-1 overflow-y-auto py-4">
{navItems.map(item => ( {navSections.map((section, i) => (
<li key={item.to}> <div key={i} className="mb-6">
<NavLink {!collapsed && (
to={item.to} <h3 className="px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
end={item.to === '/'} {section.title}
className={({ isActive }) => </h3>
`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${ )}
isActive <ul className="space-y-1 px-2">
? 'bg-blue-600 text-white' {section.items.map(item => (
: 'text-gray-400 hover:bg-gray-700 hover:text-white' <li key={item.to}>
}` <NavLink
} to={item.to}
> end={item.to === '/'}
<span>{item.icon}</span> className={({ isActive }) =>
<span>{item.label}</span> `flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
</NavLink> isActive
</li> ? 'bg-blue-600 text-white'
))} : 'text-gray-400 hover:bg-gray-700 hover:text-white'
</ul> }`
}
>
<span className="text-lg">{item.icon}</span>
{!collapsed && <span className="font-medium">{item.label}</span>}
</NavLink>
</li>
))}
</ul>
</div>
))}
</nav> </nav>
<div className="p-4 border-t border-gray-700 text-xs text-gray-500"> {/* Collapse Button */}
<p>JIRA AI Fixer v1.0.0</p> <button
<p className="mt-1">© 2026 StartData</p> onClick={() => setCollapsed(!collapsed)}
</div> className="p-4 border-t border-gray-700 text-gray-400 hover:text-white flex items-center justify-center"
>
<svg className={`w-5 h-5 transition-transform ${collapsed ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
</button>
{/* Footer */}
{!collapsed && (
<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> </aside>
{/* Main content */} {/* Main Area */}
<main className="flex-1 overflow-auto"> <div className="flex-1 flex flex-col overflow-hidden">
<Outlet /> {/* Top Bar */}
</main> <header className="h-16 bg-gray-800 border-b border-gray-700 flex items-center justify-between px-6">
<div className="flex items-center gap-4">
<h2 className="font-semibold text-lg capitalize">
{location.pathname === '/' ? 'Dashboard' : location.pathname.slice(1).replace('/', ' / ')}
</h2>
</div>
<div className="flex items-center gap-4">
<button className="relative p-2 text-gray-400 hover:text-white">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
</button>
<button className="relative p-2 text-gray-400 hover:text-white">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-sm font-bold">
RL
</div>
</div>
</header>
{/* Content */}
<main className="flex-1 overflow-auto bg-gray-900">
<Outlet />
</main>
</div>
</div> </div>
) )
} }

View File

@ -0,0 +1,28 @@
import { ReactNode } from 'react'
interface BadgeProps {
children: ReactNode
variant?: 'success' | 'warning' | 'error' | 'info' | 'default'
size?: 'sm' | 'md'
}
export function Badge({ children, variant = 'default', size = 'sm' }: BadgeProps) {
const variants = {
success: 'bg-green-500/20 text-green-400 border-green-500/30',
warning: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
error: 'bg-red-500/20 text-red-400 border-red-500/30',
info: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
default: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
}
const sizes = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-3 py-1 text-sm',
}
return (
<span className={`inline-flex items-center rounded-full border font-medium ${variants[variant]} ${sizes[size]}`}>
{children}
</span>
)
}

View File

@ -0,0 +1,50 @@
import { ButtonHTMLAttributes, ReactNode } from 'react'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
children: ReactNode
loading?: boolean
}
export function Button({
variant = 'primary',
size = 'md',
children,
loading,
className = '',
disabled,
...props
}: ButtonProps) {
const variants = {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-700 hover:bg-gray-600 text-white',
danger: 'bg-red-600 hover:bg-red-700 text-white',
ghost: 'bg-transparent hover:bg-gray-700 text-gray-300',
}
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
}
return (
<button
className={`rounded-lg font-medium transition-colors flex items-center justify-center gap-2
${variants[variant]} ${sizes[size]}
${disabled || loading ? 'opacity-50 cursor-not-allowed' : ''}
${className}`}
disabled={disabled || loading}
{...props}
>
{loading && (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
)}
{children}
</button>
)
}

View File

@ -0,0 +1,34 @@
import { ReactNode } from 'react'
interface CardProps {
children: ReactNode
className?: string
padding?: 'none' | 'sm' | 'md' | 'lg'
}
export function Card({ children, className = '', padding = 'md' }: CardProps) {
const paddings = {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
}
return (
<div className={`bg-gray-800 rounded-xl border border-gray-700 ${paddings[padding]} ${className}`}>
{children}
</div>
)
}
export function CardHeader({ children, className = '' }: { children: ReactNode, className?: string }) {
return (
<div className={`border-b border-gray-700 px-6 py-4 ${className}`}>
{children}
</div>
)
}
export function CardContent({ children, className = '' }: { children: ReactNode, className?: string }) {
return <div className={`p-6 ${className}`}>{children}</div>
}

View File

@ -0,0 +1,35 @@
import { InputHTMLAttributes, forwardRef } from 'react'
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string
error?: string
hint?: string
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, hint, className = '', ...props }, ref) => {
return (
<div className="space-y-1">
{label && (
<label className="block text-sm font-medium text-gray-300">
{label}
</label>
)}
<input
ref={ref}
className={`w-full bg-gray-700 border rounded-lg px-4 py-2.5 text-white placeholder-gray-400
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
${error ? 'border-red-500' : 'border-gray-600'}
${className}`}
{...props}
/>
{hint && !error && (
<p className="text-xs text-gray-400">{hint}</p>
)}
{error && (
<p className="text-xs text-red-400">{error}</p>
)}
</div>
)
}
)

View File

@ -0,0 +1,50 @@
import { ReactNode, useEffect } from 'react'
interface ModalProps {
open: boolean
onClose: () => void
title: string
children: ReactNode
size?: 'sm' | 'md' | 'lg' | 'xl'
}
export function Modal({ open, onClose, title, children, size = 'md' }: ModalProps) {
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [open])
if (!open) return null
const sizes = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div className={`relative bg-gray-800 rounded-xl border border-gray-700 w-full ${sizes[size]} max-h-[90vh] flex flex-col`}>
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700">
<h2 className="text-lg font-semibold">{title}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-white p-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="flex-1 overflow-y-auto p-6">
{children}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,36 @@
import { SelectHTMLAttributes, forwardRef } from 'react'
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
label?: string
error?: string
options: { value: string; label: string }[]
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ label, error, options, className = '', ...props }, ref) => {
return (
<div className="space-y-1">
{label && (
<label className="block text-sm font-medium text-gray-300">
{label}
</label>
)}
<select
ref={ref}
className={`w-full bg-gray-700 border rounded-lg px-4 py-2.5 text-white
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
${error ? 'border-red-500' : 'border-gray-600'}
${className}`}
{...props}
>
{options.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{error && (
<p className="text-xs text-red-400">{error}</p>
)}
</div>
)
}
)

View File

@ -0,0 +1,53 @@
import { ReactNode, useState, createContext, useContext } from 'react'
interface TabsContextValue {
activeTab: string
setActiveTab: (tab: string) => void
}
const TabsContext = createContext<TabsContextValue | null>(null)
export function Tabs({ defaultValue, children }: { defaultValue: string; children: ReactNode }) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
)
}
export function TabsList({ children }: { children: ReactNode }) {
return (
<div className="flex border-b border-gray-700 gap-1">
{children}
</div>
)
}
export function TabsTrigger({ value, children }: { value: string; children: ReactNode }) {
const ctx = useContext(TabsContext)
if (!ctx) return null
const isActive = ctx.activeTab === value
return (
<button
onClick={() => ctx.setActiveTab(value)}
className={`px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px
${isActive
? 'border-blue-500 text-blue-400'
: 'border-transparent text-gray-400 hover:text-white'
}`}
>
{children}
</button>
)
}
export function TabsContent({ value, children }: { value: string; children: ReactNode }) {
const ctx = useContext(TabsContext)
if (!ctx || ctx.activeTab !== value) return null
return <div className="pt-6">{children}</div>
}

View File

@ -0,0 +1,7 @@
export * from './Card'
export * from './Button'
export * from './Input'
export * from './Select'
export * from './Badge'
export * from './Modal'
export * from './Tabs'

182
src/pages/Analytics.tsx Normal file
View File

@ -0,0 +1,182 @@
import { Card } from '../components/ui/Card'
import { Select } from '../components/ui/Select'
export default function Analytics() {
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold">Analytics</h1>
<p className="text-gray-400 mt-1">Performance metrics and insights</p>
</div>
<Select
options={[
{ value: '7d', label: 'Last 7 days' },
{ value: '30d', label: 'Last 30 days' },
{ value: '90d', label: 'Last 90 days' },
{ value: 'year', label: 'This year' },
]}
className="w-40"
/>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<KPICard
title="Issues Analyzed"
value="127"
change="+23%"
trend="up"
icon="📊"
/>
<KPICard
title="PRs Created"
value="89"
change="+18%"
trend="up"
icon="🔀"
/>
<KPICard
title="Avg Resolution Time"
value="2.3h"
change="-15%"
trend="up"
icon="⏱️"
/>
<KPICard
title="Fix Accuracy"
value="94%"
change="+2%"
trend="up"
icon="🎯"
/>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<Card>
<h3 className="font-semibold mb-4">Issues Over Time</h3>
<div className="h-64 flex items-end justify-between gap-2">
{[40, 65, 45, 80, 55, 90, 70, 85, 60, 95, 75, 88].map((h, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<div
className="w-full bg-blue-500 rounded-t"
style={{ height: `${h}%` }}
/>
<span className="text-xs text-gray-500">{['J','F','M','A','M','J','J','A','S','O','N','D'][i]}</span>
</div>
))}
</div>
</Card>
<Card>
<h3 className="font-semibold mb-4">Resolution by Category</h3>
<div className="space-y-4">
{[
{ label: 'Data Truncation', value: 35, color: 'blue' },
{ label: 'Missing Validation', value: 28, color: 'green' },
{ label: 'Logic Error', value: 20, color: 'purple' },
{ label: 'Performance', value: 12, color: 'yellow' },
{ label: 'Other', value: 5, color: 'gray' },
].map(item => (
<div key={item.label}>
<div className="flex justify-between text-sm mb-1">
<span>{item.label}</span>
<span className="text-gray-400">{item.value}%</span>
</div>
<div className="h-2 bg-gray-700 rounded-full">
<div
className={`h-2 rounded-full bg-${item.color}-500`}
style={{ width: `${item.value}%`, backgroundColor: {
blue: '#3b82f6',
green: '#22c55e',
purple: '#a855f7',
yellow: '#eab308',
gray: '#6b7280',
}[item.color] }}
/>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Tables */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<h3 className="font-semibold mb-4">Top Affected Modules</h3>
<table className="w-full">
<thead>
<tr className="text-left text-sm text-gray-400 border-b border-gray-700">
<th className="pb-2">Module</th>
<th className="pb-2">Issues</th>
<th className="pb-2">Fix Rate</th>
</tr>
</thead>
<tbody className="text-sm">
{[
{ name: 'AUTH.CBL', issues: 23, rate: '96%' },
{ name: 'TRANS.CBL', issues: 18, rate: '94%' },
{ name: 'BATCH.CBL', issues: 15, rate: '93%' },
{ name: 'REPORT.CBL', issues: 12, rate: '92%' },
{ name: 'VALID.CBL', issues: 9, rate: '100%' },
].map(row => (
<tr key={row.name} className="border-b border-gray-700/50">
<td className="py-3 font-mono text-blue-400">{row.name}</td>
<td className="py-3">{row.issues}</td>
<td className="py-3 text-green-400">{row.rate}</td>
</tr>
))}
</tbody>
</table>
</Card>
<Card>
<h3 className="font-semibold mb-4">Recent Activity</h3>
<div className="space-y-4">
{[
{ action: 'PR merged', issue: 'SUPP-5', time: '2 min ago', icon: '✅' },
{ action: 'Analysis complete', issue: 'SUPP-4', time: '15 min ago', icon: '🔍' },
{ action: 'New issue', issue: 'SUPP-3', time: '1 hour ago', icon: '🎫' },
{ action: 'Rule triggered', issue: 'Auto-analyze', time: '2 hours ago', icon: '⚡' },
{ action: 'Integration sync', issue: 'Gitea', time: '3 hours ago', icon: '🔄' },
].map((item, i) => (
<div key={i} className="flex items-center gap-3">
<span className="text-xl">{item.icon}</span>
<div className="flex-1">
<p className="text-sm">{item.action}</p>
<p className="text-xs text-gray-400">{item.issue}</p>
</div>
<span className="text-xs text-gray-500">{item.time}</span>
</div>
))}
</div>
</Card>
</div>
</div>
)
}
function KPICard({ title, value, change, trend, icon }: {
title: string
value: string
change: string
trend: 'up' | 'down'
icon: string
}) {
return (
<Card>
<div className="flex items-start justify-between">
<div>
<p className="text-sm text-gray-400">{title}</p>
<p className="text-3xl font-bold mt-1">{value}</p>
<p className={`text-sm mt-1 ${trend === 'up' ? 'text-green-400' : 'text-red-400'}`}>
{change} vs last period
</p>
</div>
<span className="text-3xl">{icon}</span>
</div>
</Card>
)
}

326
src/pages/Integrations.tsx Normal file
View File

@ -0,0 +1,326 @@
import { useState } from 'react'
import { Card, CardHeader, CardContent } from '../components/ui/Card'
import { Button } from '../components/ui/Button'
import { Input } from '../components/ui/Input'
import { Select } from '../components/ui/Select'
import { Badge } from '../components/ui/Badge'
import { Modal } from '../components/ui/Modal'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/ui/Tabs'
interface Integration {
id: string
name: string
type: 'issue_tracker' | 'repository'
provider: string
status: 'connected' | 'error' | 'pending'
config: Record<string, any>
lastSync?: string
}
const mockIntegrations: Integration[] = [
{
id: '1',
name: 'TicketHub Production',
type: 'issue_tracker',
provider: 'tickethub',
status: 'connected',
config: { url: 'https://tickethub.startdata.com.br', projectKey: 'SUPP' },
lastSync: '2026-02-18T18:00:00Z',
},
{
id: '2',
name: 'Gitea StartData',
type: 'repository',
provider: 'gitea',
status: 'connected',
config: { url: 'https://gitea.startdata.com.br', org: 'startdata' },
lastSync: '2026-02-18T17:30:00Z',
},
]
const providers = {
issue_tracker: [
{ id: 'tickethub', name: 'TicketHub', icon: '🎫', color: 'blue' },
{ id: 'jira', name: 'Jira', icon: '🔵', color: 'blue' },
{ id: 'servicenow', name: 'ServiceNow', icon: '🟢', color: 'green' },
{ id: 'azure_devops', name: 'Azure DevOps', icon: '🔷', color: 'cyan' },
{ id: 'zendesk', name: 'Zendesk', icon: '💬', color: 'gray' },
],
repository: [
{ id: 'gitea', name: 'Gitea', icon: '☕', color: 'green' },
{ id: 'github', name: 'GitHub', icon: '🐙', color: 'gray' },
{ id: 'gitlab', name: 'GitLab', icon: '🦊', color: 'orange' },
{ id: 'bitbucket', name: 'Bitbucket', icon: '🪣', color: 'blue' },
{ id: 'azure_repos', name: 'Azure Repos', icon: '🔷', color: 'cyan' },
],
}
export default function Integrations() {
const [integrations] = useState<Integration[]>(mockIntegrations)
const [showAddModal, setShowAddModal] = useState(false)
const [addType, setAddType] = useState<'issue_tracker' | 'repository'>('issue_tracker')
const [selectedProvider, setSelectedProvider] = useState('')
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold">Integrations</h1>
<p className="text-gray-400 mt-1">Connect your issue trackers and code repositories</p>
</div>
<Button onClick={() => setShowAddModal(true)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Integration
</Button>
</div>
<Tabs defaultValue="all">
<TabsList>
<TabsTrigger value="all">All ({integrations.length})</TabsTrigger>
<TabsTrigger value="issue_trackers">Issue Trackers ({integrations.filter(i => i.type === 'issue_tracker').length})</TabsTrigger>
<TabsTrigger value="repositories">Repositories ({integrations.filter(i => i.type === 'repository').length})</TabsTrigger>
</TabsList>
<TabsContent value="all">
<IntegrationList integrations={integrations} />
</TabsContent>
<TabsContent value="issue_trackers">
<IntegrationList integrations={integrations.filter(i => i.type === 'issue_tracker')} />
</TabsContent>
<TabsContent value="repositories">
<IntegrationList integrations={integrations.filter(i => i.type === 'repository')} />
</TabsContent>
</Tabs>
{/* Add Integration Modal */}
<Modal open={showAddModal} onClose={() => setShowAddModal(false)} title="Add Integration" size="lg">
<div className="space-y-6">
{/* Step 1: Choose Type */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-3">Integration Type</label>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => { setAddType('issue_tracker'); setSelectedProvider(''); }}
className={`p-4 rounded-lg border-2 text-left transition-colors ${
addType === 'issue_tracker'
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<span className="text-2xl">🎫</span>
<h3 className="font-semibold mt-2">Issue Tracker</h3>
<p className="text-sm text-gray-400">JIRA, ServiceNow, TicketHub...</p>
</button>
<button
onClick={() => { setAddType('repository'); setSelectedProvider(''); }}
className={`p-4 rounded-lg border-2 text-left transition-colors ${
addType === 'repository'
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<span className="text-2xl">📁</span>
<h3 className="font-semibold mt-2">Code Repository</h3>
<p className="text-sm text-gray-400">GitHub, GitLab, Bitbucket...</p>
</button>
</div>
</div>
{/* Step 2: Choose Provider */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-3">Select Provider</label>
<div className="grid grid-cols-3 gap-3">
{providers[addType].map(provider => (
<button
key={provider.id}
onClick={() => setSelectedProvider(provider.id)}
className={`p-4 rounded-lg border text-center transition-colors ${
selectedProvider === provider.id
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<span className="text-3xl">{provider.icon}</span>
<p className="font-medium mt-2">{provider.name}</p>
</button>
))}
</div>
</div>
{/* Step 3: Configure */}
{selectedProvider && (
<ProviderConfigForm provider={selectedProvider} type={addType} />
)}
</div>
</Modal>
</div>
)
}
function IntegrationList({ integrations }: { integrations: Integration[] }) {
if (integrations.length === 0) {
return (
<Card className="text-center py-12">
<span className="text-4xl">🔌</span>
<h3 className="text-lg font-semibold mt-4">No integrations yet</h3>
<p className="text-gray-400 mt-2">Add your first integration to get started</p>
</Card>
)
}
return (
<div className="space-y-4">
{integrations.map(integration => (
<IntegrationCard key={integration.id} integration={integration} />
))}
</div>
)
}
function IntegrationCard({ integration }: { integration: Integration }) {
const [showConfig, setShowConfig] = useState(false)
const provider = [...providers.issue_tracker, ...providers.repository].find(p => p.id === integration.provider)
return (
<>
<Card padding="none">
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gray-700 rounded-lg flex items-center justify-center text-2xl">
{provider?.icon || '🔌'}
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold">{integration.name}</h3>
<Badge variant={integration.status === 'connected' ? 'success' : integration.status === 'error' ? 'error' : 'warning'}>
{integration.status}
</Badge>
</div>
<p className="text-sm text-gray-400">{provider?.name} {integration.config.url}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setShowConfig(true)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Configure
</Button>
<Button variant="ghost" size="sm">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Sync
</Button>
</div>
</div>
{integration.lastSync && (
<div className="px-4 py-2 bg-gray-700/30 text-xs text-gray-400 border-t border-gray-700">
Last synced: {new Date(integration.lastSync).toLocaleString()}
</div>
)}
</Card>
<Modal open={showConfig} onClose={() => setShowConfig(false)} title={`Configure ${integration.name}`} size="lg">
<ProviderConfigForm provider={integration.provider} type={integration.type} initialValues={integration.config} />
</Modal>
</>
)
}
function ProviderConfigForm({ provider, type, initialValues = {} }: {
provider: string
type: 'issue_tracker' | 'repository'
initialValues?: Record<string, any>
}) {
// Forms específicos por provider
if (provider === 'jira') {
return (
<div className="space-y-4">
<Input label="Instance URL" placeholder="https://yourcompany.atlassian.net" defaultValue={initialValues.url} />
<Input label="Project Key" placeholder="PROJ" defaultValue={initialValues.projectKey} />
<Input label="Email" placeholder="user@company.com" defaultValue={initialValues.email} />
<Input label="API Token" type="password" placeholder="Your Jira API token" />
<div className="border-t border-gray-700 pt-4 mt-6">
<h4 className="font-medium mb-3">Issue Types to Monitor</h4>
<div className="space-y-2">
{['Bug', 'Task', 'Story', 'Incident', 'Support Request'].map(type => (
<label key={type} className="flex items-center gap-2">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked />
<span>{type}</span>
</label>
))}
</div>
</div>
<div className="flex gap-3 pt-4">
<Button className="flex-1">Test Connection</Button>
<Button variant="primary" className="flex-1">Save Integration</Button>
</div>
</div>
)
}
if (provider === 'servicenow') {
return (
<div className="space-y-4">
<Input label="Instance URL" placeholder="https://yourcompany.service-now.com" defaultValue={initialValues.url} />
<Input label="Username" placeholder="admin" />
<Input label="Password" type="password" />
<Select
label="Table to Monitor"
options={[
{ value: 'incident', label: 'Incidents' },
{ value: 'problem', label: 'Problems' },
{ value: 'change_request', label: 'Change Requests' },
{ value: 'sc_request', label: 'Service Requests' },
]}
/>
<div className="flex gap-3 pt-4">
<Button variant="secondary" className="flex-1">Test Connection</Button>
<Button variant="primary" className="flex-1">Save Integration</Button>
</div>
</div>
)
}
if (provider === 'github' || provider === 'gitlab' || provider === 'gitea') {
return (
<div className="space-y-4">
<Input label="Instance URL" placeholder={provider === 'github' ? 'https://github.com' : 'https://your-instance.com'} defaultValue={initialValues.url} />
<Input label="Personal Access Token" type="password" placeholder="ghp_..." />
<Input label="Organization/Owner" placeholder="your-org" defaultValue={initialValues.org} />
<div className="border-t border-gray-700 pt-4 mt-6">
<h4 className="font-medium mb-3">Repositories to Index</h4>
<Input placeholder="Search repositories..." className="mb-3" />
<div className="max-h-48 overflow-y-auto space-y-2 bg-gray-700/30 rounded-lg p-3">
{['cobol-sample-app', 'main-application', 'api-gateway', 'shared-modules'].map(repo => (
<label key={repo} className="flex items-center gap-2 p-2 hover:bg-gray-700 rounded">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked={repo === 'cobol-sample-app'} />
<span className="font-mono text-sm">{repo}</span>
</label>
))}
</div>
</div>
<div className="flex gap-3 pt-4">
<Button variant="secondary" className="flex-1">Test Connection</Button>
<Button variant="primary" className="flex-1">Save Integration</Button>
</div>
</div>
)
}
// Default form
return (
<div className="space-y-4">
<Input label="Instance URL" placeholder="https://..." defaultValue={initialValues.url} />
<Input label="API Key / Token" type="password" />
<div className="flex gap-3 pt-4">
<Button variant="secondary" className="flex-1">Test Connection</Button>
<Button variant="primary" className="flex-1">Save Integration</Button>
</div>
</div>
)
}

283
src/pages/Rules.tsx Normal file
View File

@ -0,0 +1,283 @@
import { useState } from 'react'
import { Card, CardHeader, CardContent } from '../components/ui/Card'
import { Button } from '../components/ui/Button'
import { Input } from '../components/ui/Input'
import { Select } from '../components/ui/Select'
import { Badge } from '../components/ui/Badge'
import { Modal } from '../components/ui/Modal'
interface Rule {
id: string
name: string
description: string
enabled: boolean
trigger: {
type: string
conditions: any[]
}
actions: any[]
stats: {
triggered: number
successful: number
}
}
const mockRules: Rule[] = [
{
id: '1',
name: 'Auto-analyze Critical Bugs',
description: 'Automatically analyze any ticket marked as Critical priority',
enabled: true,
trigger: { type: 'ticket_created', conditions: [{ field: 'priority', op: 'eq', value: 'critical' }] },
actions: [{ type: 'analyze' }, { type: 'create_pr', auto: true }],
stats: { triggered: 45, successful: 42 },
},
{
id: '2',
name: 'COBOL Module Analysis',
description: 'Analyze tickets that mention COBOL programs',
enabled: true,
trigger: { type: 'ticket_created', conditions: [{ field: 'description', op: 'contains', value: '.CBL' }] },
actions: [{ type: 'analyze' }, { type: 'notify', channel: 'slack' }],
stats: { triggered: 23, successful: 21 },
},
{
id: '3',
name: 'Weekend Batch Jobs',
description: 'Queue analysis for weekend processing',
enabled: false,
trigger: { type: 'schedule', cron: '0 22 * * FRI' },
actions: [{ type: 'batch_analyze' }],
stats: { triggered: 8, successful: 8 },
},
]
export default function Rules() {
const [rules, setRules] = useState<Rule[]>(mockRules)
const [showAddModal, setShowAddModal] = useState(false)
const toggleRule = (id: string) => {
setRules(rules.map(r => r.id === id ? { ...r, enabled: !r.enabled } : r))
}
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold">Automation Rules</h1>
<p className="text-gray-400 mt-1">Configure when and how issues are analyzed</p>
</div>
<Button onClick={() => setShowAddModal(true)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Create Rule
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-blue-400">{rules.length}</div>
<div className="text-sm text-gray-400">Total Rules</div>
</div>
</Card>
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-green-400">{rules.filter(r => r.enabled).length}</div>
<div className="text-sm text-gray-400">Active</div>
</div>
</Card>
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-purple-400">{rules.reduce((a, r) => a + r.stats.triggered, 0)}</div>
<div className="text-sm text-gray-400">Total Triggers</div>
</div>
</Card>
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-yellow-400">
{Math.round(rules.reduce((a, r) => a + r.stats.successful, 0) / rules.reduce((a, r) => a + r.stats.triggered, 0) * 100) || 0}%
</div>
<div className="text-sm text-gray-400">Success Rate</div>
</div>
</Card>
</div>
{/* Rules List */}
<div className="space-y-4">
{rules.map(rule => (
<RuleCard key={rule.id} rule={rule} onToggle={() => toggleRule(rule.id)} />
))}
</div>
{/* Add Rule Modal */}
<Modal open={showAddModal} onClose={() => setShowAddModal(false)} title="Create Automation Rule" size="xl">
<RuleBuilder onSave={() => setShowAddModal(false)} />
</Modal>
</div>
)
}
function RuleCard({ rule, onToggle }: { rule: Rule; onToggle: () => void }) {
const [expanded, setExpanded] = useState(false)
return (
<Card padding="none">
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={onToggle}
className={`w-12 h-6 rounded-full transition-colors relative ${
rule.enabled ? 'bg-green-500' : 'bg-gray-600'
}`}
>
<span className={`absolute w-5 h-5 bg-white rounded-full top-0.5 transition-transform ${
rule.enabled ? 'translate-x-6' : 'translate-x-0.5'
}`} />
</button>
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold">{rule.name}</h3>
<Badge variant={rule.enabled ? 'success' : 'default'}>
{rule.enabled ? 'Active' : 'Disabled'}
</Badge>
</div>
<p className="text-sm text-gray-400">{rule.description}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right text-sm">
<div><span className="text-gray-400">Triggered:</span> <span className="font-medium">{rule.stats.triggered}</span></div>
<div><span className="text-gray-400">Success:</span> <span className="font-medium text-green-400">{rule.stats.successful}</span></div>
</div>
<Button variant="ghost" size="sm" onClick={() => setExpanded(!expanded)}>
<svg className={`w-4 h-4 transition-transform ${expanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</Button>
</div>
</div>
</div>
{expanded && (
<div className="px-4 pb-4 border-t border-gray-700 mt-4 pt-4">
<div className="grid grid-cols-2 gap-6">
<div>
<h4 className="text-sm font-medium text-gray-400 mb-2">Trigger</h4>
<div className="bg-gray-700/50 rounded-lg p-3 text-sm">
<div className="font-mono text-blue-400">When: {rule.trigger.type.replace('_', ' ')}</div>
{rule.trigger.conditions.map((c, i) => (
<div key={i} className="text-gray-300 mt-1">
If <span className="text-yellow-400">{c.field}</span> {c.op} <span className="text-green-400">"{c.value}"</span>
</div>
))}
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-400 mb-2">Actions</h4>
<div className="bg-gray-700/50 rounded-lg p-3 text-sm space-y-1">
{rule.actions.map((a, i) => (
<div key={i} className="flex items-center gap-2">
<span className="text-purple-400"></span>
<span>{a.type.replace('_', ' ')}</span>
{a.auto && <Badge size="sm">Auto</Badge>}
</div>
))}
</div>
</div>
</div>
<div className="flex gap-2 mt-4">
<Button variant="ghost" size="sm">Edit</Button>
<Button variant="ghost" size="sm">Duplicate</Button>
<Button variant="ghost" size="sm" className="text-red-400 hover:text-red-300">Delete</Button>
</div>
</div>
)}
</Card>
)
}
function RuleBuilder({ onSave }: { onSave: () => void }) {
return (
<div className="space-y-6">
<Input label="Rule Name" placeholder="My automation rule" />
<Input label="Description" placeholder="Describe what this rule does" />
<div className="border-t border-gray-700 pt-6">
<h3 className="font-semibold mb-4">Trigger</h3>
<Select
label="When"
options={[
{ value: 'ticket_created', label: 'A ticket is created' },
{ value: 'ticket_updated', label: 'A ticket is updated' },
{ value: 'ticket_commented', label: 'A comment is added' },
{ value: 'schedule', label: 'On a schedule' },
{ value: 'manual', label: 'Manually triggered' },
]}
/>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-300 mb-2">Conditions</label>
<div className="space-y-2">
<div className="flex gap-2">
<Select
options={[
{ value: 'priority', label: 'Priority' },
{ value: 'type', label: 'Type' },
{ value: 'title', label: 'Title' },
{ value: 'description', label: 'Description' },
{ value: 'labels', label: 'Labels' },
]}
className="w-40"
/>
<Select
options={[
{ value: 'eq', label: 'equals' },
{ value: 'neq', label: 'not equals' },
{ value: 'contains', label: 'contains' },
{ value: 'starts', label: 'starts with' },
{ value: 'regex', label: 'matches regex' },
]}
className="w-40"
/>
<Input placeholder="Value" className="flex-1" />
<Button variant="ghost" size="sm">×</Button>
</div>
</div>
<Button variant="ghost" size="sm" className="mt-2">+ Add condition</Button>
</div>
</div>
<div className="border-t border-gray-700 pt-6">
<h3 className="font-semibold mb-4">Actions</h3>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 bg-gray-700/50 rounded-lg">
<span className="text-purple-400">1.</span>
<Select
options={[
{ value: 'analyze', label: 'Analyze with AI' },
{ value: 'create_pr', label: 'Create Pull Request' },
{ value: 'comment', label: 'Add comment' },
{ value: 'assign', label: 'Assign to user' },
{ value: 'label', label: 'Add label' },
{ value: 'notify', label: 'Send notification' },
{ value: 'webhook', label: 'Call webhook' },
]}
className="flex-1"
/>
<Button variant="ghost" size="sm">×</Button>
</div>
</div>
<Button variant="ghost" size="sm" className="mt-2">+ Add action</Button>
</div>
<div className="flex gap-3 pt-4 border-t border-gray-700">
<Button variant="secondary" className="flex-1">Save as Draft</Button>
<Button variant="primary" className="flex-1" onClick={onSave}>Create Rule</Button>
</div>
</div>
)
}

View File

@ -1,119 +1,249 @@
import { Card } from '../components/ui/Card'
import { Input } from '../components/ui/Input'
import { Select } from '../components/ui/Select'
import { Button } from '../components/ui/Button'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/ui/Tabs'
export default function Settings() { export default function Settings() {
return ( return (
<div className="p-6"> <div className="p-6">
<h1 className="text-2xl font-bold mb-6">Settings</h1> <div className="mb-6">
<h1 className="text-2xl font-bold">Settings</h1>
{/* Webhook Endpoints */} <p className="text-gray-400 mt-1">Configure your JIRA AI Fixer instance</p>
<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> </div>
{/* Issue Trackers */} <Tabs defaultValue="general">
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6"> <TabsList>
<h2 className="font-semibold mb-4">Issue Tracker Integrations</h2> <TabsTrigger value="general">General</TabsTrigger>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <TabsTrigger value="ai">AI Configuration</TabsTrigger>
<TrackerCard name="TicketHub" icon="🎫" status="active" /> <TabsTrigger value="notifications">Notifications</TabsTrigger>
<TrackerCard name="JIRA" icon="🔵" status="ready" /> <TabsTrigger value="security">Security</TabsTrigger>
<TrackerCard name="ServiceNow" icon="🟢" status="ready" /> <TabsTrigger value="api">API</TabsTrigger>
<TrackerCard name="Azure DevOps" icon="🔷" status="ready" /> </TabsList>
</div>
</div> <TabsContent value="general">
<Card>
{/* AI Settings */} <h3 className="font-semibold mb-4">Organization</h3>
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6"> <div className="space-y-4 max-w-lg">
<h2 className="font-semibold mb-4">AI Configuration</h2> <Input label="Organization Name" defaultValue="StartData" />
<div className="space-y-4"> <Input label="Instance URL" defaultValue="https://jira-fixer.startdata.com.br" disabled />
<div> <Select
<label className="block text-sm text-gray-400 mb-1">LLM Provider</label> label="Timezone"
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"> options={[
<option>OpenRouter (Free Tier)</option> { value: 'America/Sao_Paulo', label: 'America/Sao_Paulo (GMT-3)' },
<option>OpenAI</option> { value: 'America/New_York', label: 'America/New_York (GMT-5)' },
<option>Anthropic</option> { value: 'UTC', label: 'UTC' },
</select> ]}
</div> />
<div> <Select
<label className="block text-sm text-gray-400 mb-1">Model</label> label="Language"
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"> options={[
<option>meta-llama/llama-3.3-70b-instruct (Free)</option> { value: 'en', label: 'English' },
<option>gpt-4-turbo</option> { value: 'pt-BR', label: 'Português (Brasil)' },
<option>claude-3-opus</option> { value: 'es', label: 'Español' },
</select> ]}
</div> />
<div> <div className="pt-4">
<label className="block text-sm text-gray-400 mb-1">Confidence Threshold</label> <Button>Save Changes</Button>
<input </div>
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> </Card>
</div> </TabsContent>
</div>
</div>
)
}
function EndpointRow({ method, path, description }: { <TabsContent value="ai">
method: string <Card>
path: string <h3 className="font-semibold mb-4">AI Model Configuration</h3>
description: string <div className="space-y-4 max-w-lg">
}) { <Select
return ( label="LLM Provider"
<div className="flex items-center gap-4 p-3 bg-gray-700/50 rounded-lg"> options={[
<span className="px-2 py-1 bg-blue-500/20 text-blue-400 rounded text-xs font-mono"> { value: 'openrouter', label: 'OpenRouter' },
{method} { value: 'openai', label: 'OpenAI' },
</span> { value: 'anthropic', label: 'Anthropic' },
<code className="flex-1 font-mono text-sm">{path}</code> { value: 'azure', label: 'Azure OpenAI' },
<span className="text-sm text-gray-400">{description}</span> ]}
<button className="text-gray-400 hover:text-white"> />
📋 <Select
</button> label="Model"
</div> options={[
) { value: 'llama-3.3-70b', label: 'Llama 3.3 70B (Free)' },
} { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' },
{ value: 'claude-3-opus', label: 'Claude 3 Opus' },
{ value: 'claude-3-sonnet', label: 'Claude 3 Sonnet' },
]}
/>
<Input label="API Key" type="password" placeholder="sk-..." />
<div className="border-t border-gray-700 pt-4 mt-6">
<h4 className="font-medium mb-3">Analysis Settings</h4>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">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 mt-1">
<span>0%</span>
<span>70% (recommended)</span>
<span>100%</span>
</div>
</div>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked />
<span className="text-sm">Auto-create PRs for high-confidence fixes</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked />
<span className="text-sm">Include code context in analysis</span>
</label>
</div>
</div>
<div className="pt-4">
<Button>Save AI Settings</Button>
</div>
</div>
</Card>
</TabsContent>
function TrackerCard({ name, icon, status }: { <TabsContent value="notifications">
name: string <Card>
icon: string <h3 className="font-semibold mb-4">Notification Preferences</h3>
status: 'active' | 'ready' <div className="space-y-6 max-w-lg">
}) { <div>
return ( <h4 className="font-medium mb-3">Email Notifications</h4>
<div className="flex items-center justify-between p-4 bg-gray-700/50 rounded-lg"> <div className="space-y-2">
<div className="flex items-center gap-3"> {[
<span className="text-2xl">{icon}</span> 'New issue received',
<span className="font-medium">{name}</span> 'Analysis complete',
</div> 'PR created',
<span className={`px-2 py-1 rounded text-xs ${ 'PR merged',
status === 'active' 'Analysis failed',
? 'bg-green-500/20 text-green-400' 'Daily summary',
: 'bg-gray-500/20 text-gray-400' 'Weekly report',
}`}> ].map(item => (
{status} <label key={item} className="flex items-center gap-2">
</span> <input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked={!item.includes('Weekly')} />
<span className="text-sm">{item}</span>
</label>
))}
</div>
</div>
<div className="border-t border-gray-700 pt-4">
<h4 className="font-medium mb-3">Slack Integration</h4>
<Input label="Webhook URL" placeholder="https://hooks.slack.com/..." />
<Select
label="Notification Level"
options={[
{ value: 'all', label: 'All events' },
{ value: 'important', label: 'Important only' },
{ value: 'critical', label: 'Critical only' },
]}
className="mt-4"
/>
</div>
<div className="pt-4">
<Button>Save Notifications</Button>
</div>
</div>
</Card>
</TabsContent>
<TabsContent value="security">
<Card>
<h3 className="font-semibold mb-4">Security Settings</h3>
<div className="space-y-6 max-w-lg">
<div>
<h4 className="font-medium mb-3">Authentication</h4>
<Select
label="SSO Provider"
options={[
{ value: 'none', label: 'None (email/password)' },
{ value: 'google', label: 'Google Workspace' },
{ value: 'okta', label: 'Okta' },
{ value: 'azure_ad', label: 'Azure AD' },
{ value: 'saml', label: 'SAML 2.0' },
]}
/>
</div>
<div className="border-t border-gray-700 pt-4">
<h4 className="font-medium mb-3">Session</h4>
<Select
label="Session Timeout"
options={[
{ value: '1h', label: '1 hour' },
{ value: '8h', label: '8 hours' },
{ value: '24h', label: '24 hours' },
{ value: '7d', label: '7 days' },
]}
/>
<label className="flex items-center gap-2 mt-4">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked />
<span className="text-sm">Require 2FA for all users</span>
</label>
</div>
<div className="border-t border-gray-700 pt-4">
<h4 className="font-medium mb-3">IP Allowlist</h4>
<Input placeholder="192.168.1.0/24, 10.0.0.0/8" />
<p className="text-xs text-gray-500 mt-1">Leave empty to allow all IPs</p>
</div>
<div className="pt-4">
<Button>Save Security Settings</Button>
</div>
</div>
</Card>
</TabsContent>
<TabsContent value="api">
<Card>
<h3 className="font-semibold mb-4">API Access</h3>
<div className="space-y-6">
<div>
<h4 className="font-medium mb-3">API Keys</h4>
<div className="bg-gray-700/50 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-mono text-sm">pk_live_</p>
<p className="text-xs text-gray-400 mt-1">Created Feb 18, 2026</p>
</div>
<div className="flex gap-2">
<Button variant="ghost" size="sm">Reveal</Button>
<Button variant="ghost" size="sm" className="text-red-400">Revoke</Button>
</div>
</div>
</div>
<Button variant="secondary" className="mt-3">Generate New API Key</Button>
</div>
<div className="border-t border-gray-700 pt-4">
<h4 className="font-medium mb-3">Webhook Endpoints</h4>
<div className="space-y-2 text-sm font-mono bg-gray-700/50 rounded-lg p-4">
<p><span className="text-blue-400">POST</span> /api/webhook/tickethub</p>
<p><span className="text-blue-400">POST</span> /api/webhook/jira</p>
<p><span className="text-blue-400">POST</span> /api/webhook/servicenow</p>
<p><span className="text-blue-400">POST</span> /api/webhook/github</p>
</div>
</div>
<div className="border-t border-gray-700 pt-4">
<h4 className="font-medium mb-3">Rate Limits</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="bg-gray-700/50 rounded-lg p-3">
<p className="text-gray-400">Requests/minute</p>
<p className="text-xl font-bold">1,000</p>
</div>
<div className="bg-gray-700/50 rounded-lg p-3">
<p className="text-gray-400">Analysis/day</p>
<p className="text-xl font-bold">500</p>
</div>
</div>
</div>
</div>
</Card>
</TabsContent>
</Tabs>
</div> </div>
) )
} }

166
src/pages/Team.tsx Normal file
View File

@ -0,0 +1,166 @@
import { useState } from 'react'
import { Card } from '../components/ui/Card'
import { Button } from '../components/ui/Button'
import { Input } from '../components/ui/Input'
import { Select } from '../components/ui/Select'
import { Badge } from '../components/ui/Badge'
import { Modal } from '../components/ui/Modal'
interface TeamMember {
id: string
name: string
email: string
role: 'admin' | 'developer' | 'viewer'
avatar: string
lastActive: string
stats: {
issuesResolved: number
prsReviewed: number
}
}
const mockTeam: TeamMember[] = [
{
id: '1',
name: 'Ricel Leite',
email: 'ricel.souza@gmail.com',
role: 'admin',
avatar: '👨‍💻',
lastActive: '2026-02-18T18:00:00Z',
stats: { issuesResolved: 45, prsReviewed: 32 },
},
{
id: '2',
name: 'AI Assistant',
email: 'ai@jira-fixer.local',
role: 'developer',
avatar: '🤖',
lastActive: '2026-02-18T18:30:00Z',
stats: { issuesResolved: 127, prsReviewed: 89 },
},
]
export default function Team() {
const [team] = useState<TeamMember[]>(mockTeam)
const [showInvite, setShowInvite] = useState(false)
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold">Team</h1>
<p className="text-gray-400 mt-1">Manage team members and permissions</p>
</div>
<Button onClick={() => setShowInvite(true)}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
</svg>
Invite Member
</Button>
</div>
{/* Role Legend */}
<div className="flex gap-4 mb-6">
<div className="flex items-center gap-2">
<Badge variant="error">Admin</Badge>
<span className="text-sm text-gray-400">Full access</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="info">Developer</Badge>
<span className="text-sm text-gray-400">Can analyze & create PRs</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="default">Viewer</Badge>
<span className="text-sm text-gray-400">Read-only access</span>
</div>
</div>
{/* Team Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{team.map(member => (
<TeamCard key={member.id} member={member} />
))}
</div>
{/* Invite Modal */}
<Modal open={showInvite} onClose={() => setShowInvite(false)} title="Invite Team Member">
<div className="space-y-4">
<Input label="Email Address" type="email" placeholder="colleague@company.com" />
<Select
label="Role"
options={[
{ value: 'viewer', label: 'Viewer - Read-only access' },
{ value: 'developer', label: 'Developer - Analyze & create PRs' },
{ value: 'admin', label: 'Admin - Full access' },
]}
/>
<div className="border-t border-gray-700 pt-4">
<h4 className="text-sm font-medium mb-3">Notification Preferences</h4>
<div className="space-y-2">
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked />
<span className="text-sm">Email on new issues</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" defaultChecked />
<span className="text-sm">Email on PR created</span>
</label>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded bg-gray-700 border-gray-600" />
<span className="text-sm">Daily summary</span>
</label>
</div>
</div>
<div className="flex gap-3 pt-4">
<Button variant="secondary" className="flex-1" onClick={() => setShowInvite(false)}>Cancel</Button>
<Button variant="primary" className="flex-1">Send Invite</Button>
</div>
</div>
</Modal>
</div>
)
}
function TeamCard({ member }: { member: TeamMember }) {
const roleColors = {
admin: 'error' as const,
developer: 'info' as const,
viewer: 'default' as const,
}
return (
<Card>
<div className="flex items-start gap-4">
<div className="w-14 h-14 bg-gray-700 rounded-full flex items-center justify-center text-3xl">
{member.avatar}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold">{member.name}</h3>
<Badge variant={roleColors[member.role]}>{member.role}</Badge>
</div>
<p className="text-sm text-gray-400">{member.email}</p>
<p className="text-xs text-gray-500 mt-1">
Active {new Date(member.lastActive).toLocaleDateString()}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-4 pt-4 border-t border-gray-700">
<div className="text-center">
<div className="text-xl font-bold text-blue-400">{member.stats.issuesResolved}</div>
<div className="text-xs text-gray-400">Issues Resolved</div>
</div>
<div className="text-center">
<div className="text-xl font-bold text-purple-400">{member.stats.prsReviewed}</div>
<div className="text-xs text-gray-400">PRs Reviewed</div>
</div>
</div>
<div className="flex gap-2 mt-4">
<Button variant="ghost" size="sm" className="flex-1">Edit</Button>
<Button variant="ghost" size="sm" className="flex-1 text-red-400">Remove</Button>
</div>
</Card>
)
}