From cc24d759823e031ae800fe30e1b398566f06edd1 Mon Sep 17 00:00:00 2001 From: Ricel Leite Date: Wed, 18 Feb 2026 18:31:27 -0300 Subject: [PATCH] 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 --- src/App.tsx | 8 + src/components/Layout.tsx | 159 +++++++++++----- src/components/ui/Badge.tsx | 28 +++ src/components/ui/Button.tsx | 50 +++++ src/components/ui/Card.tsx | 34 ++++ src/components/ui/Input.tsx | 35 ++++ src/components/ui/Modal.tsx | 50 +++++ src/components/ui/Select.tsx | 36 ++++ src/components/ui/Tabs.tsx | 53 ++++++ src/components/ui/index.ts | 7 + src/pages/Analytics.tsx | 182 ++++++++++++++++++ src/pages/Integrations.tsx | 326 ++++++++++++++++++++++++++++++++ src/pages/Rules.tsx | 283 ++++++++++++++++++++++++++++ src/pages/Settings.tsx | 348 ++++++++++++++++++++++++----------- src/pages/Team.tsx | 166 +++++++++++++++++ 15 files changed, 1615 insertions(+), 150 deletions(-) create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/Modal.tsx create mode 100644 src/components/ui/Select.tsx create mode 100644 src/components/ui/Tabs.tsx create mode 100644 src/components/ui/index.ts create mode 100644 src/pages/Analytics.tsx create mode 100644 src/pages/Integrations.tsx create mode 100644 src/pages/Rules.tsx create mode 100644 src/pages/Team.tsx diff --git a/src/App.tsx b/src/App.tsx index e192f44..00c3774 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { }> } /> + } /> } /> } /> } /> + } /> + } /> + } /> } /> diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 9cbe88d..6b98119 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -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 (
{/* Sidebar */} -
) } diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..d899f9d --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -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 ( + + {children} + + ) +} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..faa6886 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,50 @@ +import { ButtonHTMLAttributes, ReactNode } from 'react' + +interface ButtonProps extends ButtonHTMLAttributes { + 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 ( + + ) +} diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx new file mode 100644 index 0000000..e804876 --- /dev/null +++ b/src/components/ui/Card.tsx @@ -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 ( +
+ {children} +
+ ) +} + +export function CardHeader({ children, className = '' }: { children: ReactNode, className?: string }) { + return ( +
+ {children} +
+ ) +} + +export function CardContent({ children, className = '' }: { children: ReactNode, className?: string }) { + return
{children}
+} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..5ee7214 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,35 @@ +import { InputHTMLAttributes, forwardRef } from 'react' + +interface InputProps extends InputHTMLAttributes { + label?: string + error?: string + hint?: string +} + +export const Input = forwardRef( + ({ label, error, hint, className = '', ...props }, ref) => { + return ( +
+ {label && ( + + )} + + {hint && !error && ( +

{hint}

+ )} + {error && ( +

{error}

+ )} +
+ ) + } +) diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx new file mode 100644 index 0000000..877cee5 --- /dev/null +++ b/src/components/ui/Modal.tsx @@ -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 ( +
+
+
+
+

{title}

+ +
+
+ {children} +
+
+
+ ) +} diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx new file mode 100644 index 0000000..c3a64de --- /dev/null +++ b/src/components/ui/Select.tsx @@ -0,0 +1,36 @@ +import { SelectHTMLAttributes, forwardRef } from 'react' + +interface SelectProps extends SelectHTMLAttributes { + label?: string + error?: string + options: { value: string; label: string }[] +} + +export const Select = forwardRef( + ({ label, error, options, className = '', ...props }, ref) => { + return ( +
+ {label && ( + + )} + + {error && ( +

{error}

+ )} +
+ ) + } +) diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..8c17352 --- /dev/null +++ b/src/components/ui/Tabs.tsx @@ -0,0 +1,53 @@ +import { ReactNode, useState, createContext, useContext } from 'react' + +interface TabsContextValue { + activeTab: string + setActiveTab: (tab: string) => void +} + +const TabsContext = createContext(null) + +export function Tabs({ defaultValue, children }: { defaultValue: string; children: ReactNode }) { + const [activeTab, setActiveTab] = useState(defaultValue) + + return ( + + {children} + + ) +} + +export function TabsList({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ) +} + +export function TabsTrigger({ value, children }: { value: string; children: ReactNode }) { + const ctx = useContext(TabsContext) + if (!ctx) return null + + const isActive = ctx.activeTab === value + + return ( + + ) +} + +export function TabsContent({ value, children }: { value: string; children: ReactNode }) { + const ctx = useContext(TabsContext) + if (!ctx || ctx.activeTab !== value) return null + + return
{children}
+} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..279f54b --- /dev/null +++ b/src/components/ui/index.ts @@ -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' diff --git a/src/pages/Analytics.tsx b/src/pages/Analytics.tsx new file mode 100644 index 0000000..9cf6700 --- /dev/null +++ b/src/pages/Analytics.tsx @@ -0,0 +1,182 @@ +import { Card } from '../components/ui/Card' +import { Select } from '../components/ui/Select' + +export default function Analytics() { + return ( +
+
+
+

Analytics

+

Performance metrics and insights

+
+ + + + +
+

Issue Types to Monitor

+
+ {['Bug', 'Task', 'Story', 'Incident', 'Support Request'].map(type => ( + + ))} +
+
+
+ + +
+
+ ) + } + + if (provider === 'servicenow') { + return ( +
+ + + + + + +
+

Repositories to Index

+ +
+ {['cobol-sample-app', 'main-application', 'api-gateway', 'shared-modules'].map(repo => ( + + ))} +
+
+
+ + +
+
+ ) + } + + // Default form + return ( +
+ + +
+ + +
+
+ ) +} diff --git a/src/pages/Rules.tsx b/src/pages/Rules.tsx new file mode 100644 index 0000000..c3c64ac --- /dev/null +++ b/src/pages/Rules.tsx @@ -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(mockRules) + const [showAddModal, setShowAddModal] = useState(false) + + const toggleRule = (id: string) => { + setRules(rules.map(r => r.id === id ? { ...r, enabled: !r.enabled } : r)) + } + + return ( +
+
+
+

Automation Rules

+

Configure when and how issues are analyzed

+
+ +
+ + {/* Stats */} +
+ +
+
{rules.length}
+
Total Rules
+
+
+ +
+
{rules.filter(r => r.enabled).length}
+
Active
+
+
+ +
+
{rules.reduce((a, r) => a + r.stats.triggered, 0)}
+
Total Triggers
+
+
+ +
+
+ {Math.round(rules.reduce((a, r) => a + r.stats.successful, 0) / rules.reduce((a, r) => a + r.stats.triggered, 0) * 100) || 0}% +
+
Success Rate
+
+
+
+ + {/* Rules List */} +
+ {rules.map(rule => ( + toggleRule(rule.id)} /> + ))} +
+ + {/* Add Rule Modal */} + setShowAddModal(false)} title="Create Automation Rule" size="xl"> + setShowAddModal(false)} /> + +
+ ) +} + +function RuleCard({ rule, onToggle }: { rule: Rule; onToggle: () => void }) { + const [expanded, setExpanded] = useState(false) + + return ( + +
+
+
+ +
+
+

{rule.name}

+ + {rule.enabled ? 'Active' : 'Disabled'} + +
+

{rule.description}

+
+
+
+
+
Triggered: {rule.stats.triggered}
+
Success: {rule.stats.successful}
+
+ +
+
+
+ + {expanded && ( +
+
+
+

Trigger

+
+
When: {rule.trigger.type.replace('_', ' ')}
+ {rule.trigger.conditions.map((c, i) => ( +
+ If {c.field} {c.op} "{c.value}" +
+ ))} +
+
+
+

Actions

+
+ {rule.actions.map((a, i) => ( +
+ β†’ + {a.type.replace('_', ' ')} + {a.auto && Auto} +
+ ))} +
+
+
+
+ + + +
+
+ )} +
+ ) +} + +function RuleBuilder({ onSave }: { onSave: () => void }) { + return ( +
+ + + +
+

Trigger

+ + + +
+
+ +
+
+ +
+

Actions

+
+
+ 1. + - - - - -
-
- - -
-
- - -
- 0% - 70% - 100% + + + + General + AI Configuration + Notifications + Security + API + + + + +

Organization

+
+ + + +
+ +
-
-
-
-
- ) -} + + -function EndpointRow({ method, path, description }: { - method: string - path: string - description: string -}) { - return ( -
- - {method} - - {path} - {description} - -
- ) -} + + +

AI Model Configuration

+
+ + + +
+

Analysis Settings

+
+
+ + +
+ 0% + 70% (recommended) + 100% +
+
+ + +
+
+
+ +
+
+
+
-function TrackerCard({ name, icon, status }: { - name: string - icon: string - status: 'active' | 'ready' -}) { - return ( -
-
- {icon} - {name} -
- - {status} - + + +

Notification Preferences

+
+
+

Email Notifications

+
+ {[ + 'New issue received', + 'Analysis complete', + 'PR created', + 'PR merged', + 'Analysis failed', + 'Daily summary', + 'Weekly report', + ].map(item => ( + + ))} +
+
+ +
+

Slack Integration

+ + +
+ +
+

Session

+ + Require 2FA for all users + +
+ +
+

IP Allowlist

+ +

Leave empty to allow all IPs

+
+ +
+ +
+
+
+
+ + + +

API Access

+
+
+

API Keys

+
+
+
+

pk_live_β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’

+

Created Feb 18, 2026

+
+
+ + +
+
+
+ +
+ +
+

Webhook Endpoints

+
+

POST /api/webhook/tickethub

+

POST /api/webhook/jira

+

POST /api/webhook/servicenow

+

POST /api/webhook/github

+
+
+ +
+

Rate Limits

+
+
+

Requests/minute

+

1,000

+
+
+

Analysis/day

+

500

+
+
+
+
+
+
+
) } diff --git a/src/pages/Team.tsx b/src/pages/Team.tsx new file mode 100644 index 0000000..38c2c94 --- /dev/null +++ b/src/pages/Team.tsx @@ -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(mockTeam) + const [showInvite, setShowInvite] = useState(false) + + return ( +
+
+
+

Team

+

Manage team members and permissions

+
+ +
+ + {/* Role Legend */} +
+
+ Admin + Full access +
+
+ Developer + Can analyze & create PRs +
+
+ Viewer + Read-only access +
+
+ + {/* Team Grid */} +
+ {team.map(member => ( + + ))} +
+ + {/* Invite Modal */} + setShowInvite(false)} title="Invite Team Member"> +
+ + + Email on new issues + + + +
+
+
+ + +
+ + + + ) +} + +function TeamCard({ member }: { member: TeamMember }) { + const roleColors = { + admin: 'error' as const, + developer: 'info' as const, + viewer: 'default' as const, + } + + return ( + +
+
+ {member.avatar} +
+
+
+

{member.name}

+ {member.role} +
+

{member.email}

+

+ Active {new Date(member.lastActive).toLocaleDateString()} +

+
+
+ +
+
+
{member.stats.issuesResolved}
+
Issues Resolved
+
+
+
{member.stats.prsReviewed}
+
PRs Reviewed
+
+
+ +
+ + +
+
+ ) +}