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:
parent
fd966983a3
commit
cc24d75982
|
|
@ -4,6 +4,10 @@ import Dashboard from './pages/Dashboard'
|
|||
import Issues from './pages/Issues'
|
||||
import IssueDetail from './pages/IssueDetail'
|
||||
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'
|
||||
|
||||
export default function App() {
|
||||
|
|
@ -11,9 +15,13 @@ export default function App() {
|
|||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="analytics" element={<Analytics />} />
|
||||
<Route path="issues" element={<Issues />} />
|
||||
<Route path="issues/:id" element={<IssueDetail />} />
|
||||
<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>
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{ to: '/', label: 'Dashboard', icon: '📊' },
|
||||
{ to: '/issues', label: 'Issues', icon: '🎫' },
|
||||
{ to: '/repositories', label: 'Repositories', icon: '📁' },
|
||||
{ to: '/settings', label: 'Settings', icon: '⚙️' },
|
||||
const navSections = [
|
||||
{
|
||||
title: 'Overview',
|
||||
items: [
|
||||
{ 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() {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
|
||||
<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="flex items-center gap-2">
|
||||
<span className="text-2xl">🤖</span>
|
||||
<div>
|
||||
<h1 className="font-bold">JIRA AI Fixer</h1>
|
||||
<p className="text-xs text-gray-400">Portal</p>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div>
|
||||
<h1 className="font-bold text-lg">JIRA AI Fixer</h1>
|
||||
<p className="text-xs text-gray-400">Enterprise</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4">
|
||||
<ul className="space-y-1">
|
||||
{navItems.map(item => (
|
||||
<li key={item.to}>
|
||||
<NavLink
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-700 hover:text-white'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span>{item.icon}</span>
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-y-auto py-4">
|
||||
{navSections.map((section, i) => (
|
||||
<div key={i} className="mb-6">
|
||||
{!collapsed && (
|
||||
<h3 className="px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
|
||||
{section.title}
|
||||
</h3>
|
||||
)}
|
||||
<ul className="space-y-1 px-2">
|
||||
{section.items.map(item => (
|
||||
<li key={item.to}>
|
||||
<NavLink
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-400 hover:bg-gray-700 hover:text-white'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
{!collapsed && <span className="font-medium">{item.label}</span>}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-700 text-xs text-gray-500">
|
||||
<p>JIRA AI Fixer v1.0.0</p>
|
||||
<p className="mt-1">© 2026 StartData</p>
|
||||
</div>
|
||||
{/* Collapse Button */}
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
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>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
{/* Main Area */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Top Bar */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">Settings</h1>
|
||||
|
||||
{/* Webhook Endpoints */}
|
||||
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
|
||||
<h2 className="font-semibold mb-4">Webhook Endpoints</h2>
|
||||
<div className="space-y-3">
|
||||
<EndpointRow
|
||||
method="POST"
|
||||
path="/api/webhook/tickethub"
|
||||
description="Receive ticket events from TicketHub"
|
||||
/>
|
||||
<EndpointRow
|
||||
method="POST"
|
||||
path="/api/webhook/jira"
|
||||
description="Receive issue events from JIRA"
|
||||
/>
|
||||
<EndpointRow
|
||||
method="POST"
|
||||
path="/api/webhook/servicenow"
|
||||
description="Receive incident events from ServiceNow"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold">Settings</h1>
|
||||
<p className="text-gray-400 mt-1">Configure your JIRA AI Fixer instance</p>
|
||||
</div>
|
||||
|
||||
{/* Issue Trackers */}
|
||||
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6 mb-6">
|
||||
<h2 className="font-semibold mb-4">Issue Tracker Integrations</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<TrackerCard name="TicketHub" icon="🎫" status="active" />
|
||||
<TrackerCard name="JIRA" icon="🔵" status="ready" />
|
||||
<TrackerCard name="ServiceNow" icon="🟢" status="ready" />
|
||||
<TrackerCard name="Azure DevOps" icon="🔷" status="ready" />
|
||||
</div>
|
||||
</div>
|
||||
<Tabs defaultValue="general">
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="ai">AI Configuration</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
<TabsTrigger value="api">API</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* AI Settings */}
|
||||
<div className="bg-gray-800 rounded-xl border border-gray-700 p-6">
|
||||
<h2 className="font-semibold mb-4">AI Configuration</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">LLM Provider</label>
|
||||
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2">
|
||||
<option>OpenRouter (Free Tier)</option>
|
||||
<option>OpenAI</option>
|
||||
<option>Anthropic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Model</label>
|
||||
<select className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2">
|
||||
<option>meta-llama/llama-3.3-70b-instruct (Free)</option>
|
||||
<option>gpt-4-turbo</option>
|
||||
<option>claude-3-opus</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1">Confidence Threshold</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
defaultValue="70"
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>0%</span>
|
||||
<span>70%</span>
|
||||
<span>100%</span>
|
||||
<TabsContent value="general">
|
||||
<Card>
|
||||
<h3 className="font-semibold mb-4">Organization</h3>
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<Input label="Organization Name" defaultValue="StartData" />
|
||||
<Input label="Instance URL" defaultValue="https://jira-fixer.startdata.com.br" disabled />
|
||||
<Select
|
||||
label="Timezone"
|
||||
options={[
|
||||
{ value: 'America/Sao_Paulo', label: 'America/Sao_Paulo (GMT-3)' },
|
||||
{ value: 'America/New_York', label: 'America/New_York (GMT-5)' },
|
||||
{ value: 'UTC', label: 'UTC' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Language"
|
||||
options={[
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'pt-BR', label: 'Português (Brasil)' },
|
||||
{ value: 'es', label: 'Español' },
|
||||
]}
|
||||
/>
|
||||
<div className="pt-4">
|
||||
<Button>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
function EndpointRow({ method, path, description }: {
|
||||
method: string
|
||||
path: string
|
||||
description: string
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-4 p-3 bg-gray-700/50 rounded-lg">
|
||||
<span className="px-2 py-1 bg-blue-500/20 text-blue-400 rounded text-xs font-mono">
|
||||
{method}
|
||||
</span>
|
||||
<code className="flex-1 font-mono text-sm">{path}</code>
|
||||
<span className="text-sm text-gray-400">{description}</span>
|
||||
<button className="text-gray-400 hover:text-white">
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<TabsContent value="ai">
|
||||
<Card>
|
||||
<h3 className="font-semibold mb-4">AI Model Configuration</h3>
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<Select
|
||||
label="LLM Provider"
|
||||
options={[
|
||||
{ value: 'openrouter', label: 'OpenRouter' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
{ value: 'anthropic', label: 'Anthropic' },
|
||||
{ value: 'azure', label: 'Azure OpenAI' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Model"
|
||||
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-..." />
|
||||
|
||||
function TrackerCard({ name, icon, status }: {
|
||||
name: string
|
||||
icon: string
|
||||
status: 'active' | 'ready'
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 bg-gray-700/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{icon}</span>
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
status === 'active'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{status}
|
||||
</span>
|
||||
<div 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>
|
||||
|
||||
<TabsContent value="notifications">
|
||||
<Card>
|
||||
<h3 className="font-semibold mb-4">Notification Preferences</h3>
|
||||
<div className="space-y-6 max-w-lg">
|
||||
<div>
|
||||
<h4 className="font-medium mb-3">Email Notifications</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
'New issue received',
|
||||
'Analysis complete',
|
||||
'PR created',
|
||||
'PR merged',
|
||||
'Analysis failed',
|
||||
'Daily summary',
|
||||
'Weekly report',
|
||||
].map(item => (
|
||||
<label key={item} className="flex items-center gap-2">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue