diff --git a/tests/e2e/test_dashboard.spec.ts b/tests/e2e/test_dashboard.spec.ts index 79028fa0..7c41ebcd 100644 --- a/tests/e2e/test_dashboard.spec.ts +++ b/tests/e2e/test_dashboard.spec.ts @@ -67,10 +67,7 @@ test.describe('Dashboard - Sprint 10 Features', () => { await expect(page.locator('[data-testid="review-score-chart"]')).toBeAttached(); }); - test.skip('should display quality gates panel', async () => { - // SKIP: Quality gates panel requires task selection and is currently disabled in Dashboard - // This test will be enabled when quality gates feature is fully implemented - + test('should display quality gates panel', async () => { // Navigate to quality gates section const qualityGatesPanel = page.locator('[data-testid="quality-gates-panel"]'); diff --git a/web-ui/src/api/qualityGates.ts b/web-ui/src/api/qualityGates.ts index 09a7653d..5292c0ae 100644 --- a/web-ui/src/api/qualityGates.ts +++ b/web-ui/src/api/qualityGates.ts @@ -19,21 +19,26 @@ const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; * Fetch quality gate status for a task * * @param taskId - Task ID to get quality gate status for + * @param projectId - Optional project ID for multi-project scoping * @returns Promise resolving to QualityGateStatus or null if not found * @throws Error if request fails */ export async function fetchQualityGateStatus( - taskId: number + taskId: number, + projectId?: number ): Promise { - const response = await fetch( - `${API_BASE_URL}/api/tasks/${taskId}/quality-gates`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - } - ); + // Build URL with optional project_id query parameter + const url = new URL(`${API_BASE_URL}/api/tasks/${taskId}/quality-gates`); + if (projectId !== undefined && projectId > 0) { + url.searchParams.append('project_id', projectId.toString()); + } + + const response = await fetch(url.toString(), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); if (response.status === 404) { return null; // No quality gate status exists yet diff --git a/web-ui/src/components/Dashboard.tsx b/web-ui/src/components/Dashboard.tsx index 76f9e0ed..2b725132 100644 --- a/web-ui/src/components/Dashboard.tsx +++ b/web-ui/src/components/Dashboard.tsx @@ -29,6 +29,7 @@ import { SessionStatus } from './SessionStatus'; import CheckpointList from './checkpoints/CheckpointList'; import CostDashboard from './metrics/CostDashboard'; import ReviewSummary from './reviews/ReviewSummary'; +import { QualityGatesPanel } from './quality-gates'; import TaskStats from './tasks/TaskStats'; interface DashboardProps { @@ -436,13 +437,12 @@ export default function Dashboard({ projectId }: DashboardProps) { {/* Quality Gates Panel (Sprint 10) */} - {/* Note: QualityGateStatus requires taskId, not projectId. Disabled for now until task is selected */} - {/*
+

βœ… Quality Gates

- +
-
*/} +
{/* Checkpoint Panel (Sprint 10) */}
diff --git a/web-ui/src/components/quality-gates/GateStatusIndicator.tsx b/web-ui/src/components/quality-gates/GateStatusIndicator.tsx new file mode 100644 index 00000000..dbfcfd72 --- /dev/null +++ b/web-ui/src/components/quality-gates/GateStatusIndicator.tsx @@ -0,0 +1,62 @@ +/** + * GateStatusIndicator Component + * + * Displays status for an individual quality gate with icon, name, and status badge + * Used in QualityGatesPanel to show overview of all gate types + */ + +'use client'; + +import type { GateTypeE2E, QualityGateStatusValue } from '@/types/qualityGates'; +import { getGateIcon, getGateName, getStatusClasses, getStatusIcon } from '@/lib/qualityGateUtils'; + +interface GateStatusIndicatorProps { + gateType: GateTypeE2E; + status: QualityGateStatusValue; + testId?: string; +} + +/** + * GateStatusIndicator Component + * + * Shows individual gate status in a card layout with proper accessibility + */ +export default function GateStatusIndicator({ + gateType, + status, + testId, +}: GateStatusIndicatorProps) { + const gateName = getGateName(gateType); + const statusText = status ?? 'pending'; + + return ( +
+ {/* Gate Icon */} + + + {/* Gate Name */} +
+ {gateName} +
+ + {/* Status Badge */} +
+ + {statusText} +
+
+ ); +} diff --git a/web-ui/src/components/quality-gates/QualityGateStatus.tsx b/web-ui/src/components/quality-gates/QualityGateStatus.tsx index 52e732d8..abbeb837 100644 --- a/web-ui/src/components/quality-gates/QualityGateStatus.tsx +++ b/web-ui/src/components/quality-gates/QualityGateStatus.tsx @@ -16,9 +16,9 @@ import { useState, useEffect, useCallback } from 'react'; import type { QualityGateStatus as QualityGateStatusType, QualityGateFailure, - QualityGateStatusValue, } from '@/types/qualityGates'; import { fetchQualityGateStatus, triggerQualityGates } from '@/api/qualityGates'; +import { getStatusClasses, getSeverityClasses, getGateIcon, getStatusIcon } from '@/lib/qualityGateUtils'; interface QualityGateStatusProps { taskId: number; @@ -85,72 +85,6 @@ export default function QualityGateStatus({ } }; - // Get status badge classes - const getStatusClasses = (statusValue: QualityGateStatusValue): string => { - switch (statusValue) { - case 'passed': - return 'bg-green-100 text-green-800 border-green-300'; - case 'failed': - return 'bg-red-100 text-red-800 border-red-300'; - case 'running': - return 'bg-yellow-100 text-yellow-800 border-yellow-300'; - case 'pending': - return 'bg-gray-100 text-gray-800 border-gray-300'; - default: - return 'bg-gray-100 text-gray-800 border-gray-300'; - } - }; - - // Get severity badge classes - const getSeverityClasses = (severity: string): string => { - switch (severity) { - case 'critical': - return 'bg-red-100 text-red-900 border-red-300'; - case 'high': - return 'bg-orange-100 text-orange-900 border-orange-300'; - case 'medium': - return 'bg-yellow-100 text-yellow-900 border-yellow-300'; - case 'low': - return 'bg-blue-100 text-blue-900 border-blue-300'; - default: - return 'bg-gray-100 text-gray-900 border-gray-300'; - } - }; - - // Get gate icon - const getGateIcon = (gate: string): string => { - switch (gate) { - case 'tests': - return 'πŸ§ͺ'; - case 'type_check': - return 'πŸ“'; - case 'coverage': - return 'πŸ“Š'; - case 'code_review': - return 'πŸ”'; - case 'linting': - return '✨'; - default: - return 'βš™οΈ'; - } - }; - - // Get status icon - const getStatusIcon = (statusValue: QualityGateStatusValue): string => { - switch (statusValue) { - case 'passed': - return 'βœ…'; - case 'failed': - return '❌'; - case 'running': - return '⏳'; - case 'pending': - return '⏸️'; - default: - return '❓'; - } - }; - // Loading state if (loading) { return ( diff --git a/web-ui/src/components/quality-gates/QualityGatesPanel.tsx b/web-ui/src/components/quality-gates/QualityGatesPanel.tsx new file mode 100644 index 00000000..4007de70 --- /dev/null +++ b/web-ui/src/components/quality-gates/QualityGatesPanel.tsx @@ -0,0 +1,276 @@ +/** + * QualityGatesPanel Component + * + * Dashboard-level panel for quality gates with task selection + * Displays overview of all gate types and detailed status view + */ + +'use client'; + +import { useState, useEffect, useMemo, useRef } from 'react'; +import type { Task } from '@/types/agentState'; +import type { + QualityGateStatus as QualityGateStatusType, + GateTypeE2E, + QualityGateStatusValue, +} from '@/types/qualityGates'; +import { mapE2EToBackend, ALL_GATE_TYPES_E2E } from '@/types/qualityGates'; +import { fetchQualityGateStatus } from '@/api/qualityGates'; +import QualityGateStatus from './QualityGateStatus'; +import GateStatusIndicator from './GateStatusIndicator'; + +/** + * Props for QualityGatesPanel component + */ +interface QualityGatesPanelProps { + /** Project ID for API scoping and multi-project support */ + projectId: number; + /** List of tasks from Dashboard state (filters for completed/in_progress) */ + tasks: Task[]; +} + +/** + * Get individual gate status from quality gate status response + * + * IMPORTANT: This function uses a conservative approach: + * - Returns 'failed' if gate has explicit failures + * - Returns 'running' if overall status is running + * - Returns 'passed' ONLY if overall status is passed AND no failures exist for this gate + * - Returns 'pending' (null) for all other cases (no explicit status for this gate) + * + * This prevents showing false positives where a gate appears passed when it hasn't run. + */ +function getGateStatus( + status: QualityGateStatusType | null, + gateType: GateTypeE2E +): QualityGateStatusValue { + // No status available - gate hasn't run yet + if (!status) { + return null; // pending + } + + // Map E2E type to backend type for lookup + const backendType = mapE2EToBackend(gateType); + + // Check if this specific gate has failures + const hasFailure = status.failures.some(f => f.gate === backendType); + + if (hasFailure) { + return 'failed'; + } + + // If overall status is running, inherit that + if (status.status === 'running') { + return 'running'; + } + + // KNOWN LIMITATION: Shows all gates as 'passed' if overall status is 'passed' + // This may create false positives if only some gates have run. + // IDEAL: Backend should return 'gates_evaluated: string[]' to track which gates actually ran + // WORKAROUND: Assumes if overall status is 'passed' and no failures exist, gate passed + // TODO: Add 'gates_evaluated' field to QualityGateStatus (backend enhancement) + if (status.status === 'passed') { + return 'passed'; + } + + // Default to pending for any other case (including null overall status) + // This is conservative: better to show pending than incorrectly show passed + return null; // pending +} + +/** + * QualityGatesPanel Component + * + * Main panel for quality gates with task selection and gate overview + */ +export default function QualityGatesPanel({ + projectId, + tasks, +}: QualityGatesPanelProps) { + const [selectedTaskId, setSelectedTaskId] = useState(null); + const [gateStatus, setGateStatus] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Track if we've already auto-selected a task to prevent unnecessary updates + const hasAutoSelectedRef = useRef(false); + + // Filter tasks that are completed or in_progress (candidates for quality gates) + const eligibleTasks = useMemo(() => { + return tasks.filter(t => t.status === 'completed' || t.status === 'in_progress'); + }, [tasks]); + + // Auto-select first eligible task if none selected (optimized with useRef) + useEffect(() => { + // Reset flag if no eligible tasks (allows re-selection when tasks are re-added) + if (eligibleTasks.length === 0) { + hasAutoSelectedRef.current = false; + } + + if (!hasAutoSelectedRef.current && eligibleTasks.length > 0 && selectedTaskId === null) { + setSelectedTaskId(eligibleTasks[0].id); + hasAutoSelectedRef.current = true; + } + }, [eligibleTasks, selectedTaskId]); + + // Fetch quality gate status when task is selected + useEffect(() => { + if (selectedTaskId === null) { + setGateStatus(null); + setError(null); + return; + } + + // Type-safe: TypeScript now knows selectedTaskId is not null + const taskId = selectedTaskId; + let isMounted = true; // Cleanup flag to prevent state updates on unmounted component + const abortController = new AbortController(); // Cancel in-flight requests on cleanup + + async function fetchStatus() { + setLoading(true); + setError(null); + try { + // Note: fetchQualityGateStatus doesn't yet support AbortSignal + // Using isMounted flag as fallback to prevent stale updates + const status = await fetchQualityGateStatus(taskId, projectId); + if (isMounted && !abortController.signal.aborted) { + setGateStatus(status); + } + } catch (err) { + // Ignore errors from aborted requests + if (abortController.signal.aborted) { + return; + } + if (isMounted) { + // Provide specific error messages based on error type + let errorMessage = 'Failed to fetch quality gate status'; + if (err instanceof Error) { + if (err.message.includes('404')) { + errorMessage = 'No quality gate data found for this task'; + } else if (err.message.toLowerCase().includes('network') || err.message.toLowerCase().includes('fetch')) { + errorMessage = 'Network error. Please check your connection.'; + } else { + errorMessage = err.message; + } + } + console.error('Quality gate fetch error:', err); + setError(errorMessage); + setGateStatus(null); + } + } finally { + if (isMounted && !abortController.signal.aborted) { + setLoading(false); + } + } + } + + fetchStatus(); + + return () => { + abortController.abort(); // Cancel in-flight request + isMounted = false; // Cleanup on unmount + }; + }, [selectedTaskId, projectId]); + + // All gate types in order (from shared constant) + const gateTypes = ALL_GATE_TYPES_E2E; + + // No eligible tasks + if (eligibleTasks.length === 0) { + return ( +
+
+ + + No tasks available for quality gate evaluation. Complete or start a task first. + +
+
+ ); + } + + return ( +
+ {/* Task Selector */} +
+ + +
+ + {/* Error State */} + {error && ( +
+
+ +
+

Error Loading Quality Gates

+

{error}

+
+
+
+ )} + + {/* Loading State */} + {loading ? ( +
+ + Loading quality gates... +
+ ) : !error && ( + <> + {/* Gate Status Indicators Grid */} + {/* Grid layout matches gate count (5): 2 cols mobile, 3 cols tablet, 5 cols desktop */} +
+ {gateTypes.map(gateType => ( + + ))} +
+ + {/* Detailed Status View */} + {selectedTaskId && ( +
+

Detailed Status

+ +
+ )} + + )} +
+ ); +} diff --git a/web-ui/src/components/quality-gates/index.ts b/web-ui/src/components/quality-gates/index.ts new file mode 100644 index 00000000..d15be9c0 --- /dev/null +++ b/web-ui/src/components/quality-gates/index.ts @@ -0,0 +1,7 @@ +/** + * Quality Gates Components Barrel Export + */ + +export { default as QualityGateStatus } from './QualityGateStatus'; +export { default as QualityGatesPanel } from './QualityGatesPanel'; +export { default as GateStatusIndicator } from './GateStatusIndicator'; diff --git a/web-ui/src/lib/qualityGateUtils.ts b/web-ui/src/lib/qualityGateUtils.ts new file mode 100644 index 00000000..f89c556b --- /dev/null +++ b/web-ui/src/lib/qualityGateUtils.ts @@ -0,0 +1,132 @@ +/** + * Shared Quality Gate Utilities + * Centralized helpers for gate icons, names, status classes, etc. + */ + +import type { GateTypeE2E, QualityGateStatusValue } from '@/types/qualityGates'; + +/** + * Get the icon emoji for a quality gate type + * @param gateType - The gate type (E2E or backend naming) + * @returns Icon emoji string + * @example + * getGateIcon('tests') // returns 'πŸ§ͺ' + * getGateIcon('type_check') // returns 'πŸ“' + */ +export function getGateIcon(gateType: GateTypeE2E | string): string { + switch (gateType) { + case 'tests': + return 'πŸ§ͺ'; + case 'coverage': + return 'πŸ“Š'; + case 'type-check': + case 'type_check': + return 'πŸ“'; + case 'lint': + case 'linting': + return '✨'; + case 'review': + case 'code_review': + return 'πŸ”'; + default: + return 'βš™οΈ'; + } +} + +/** + * Get the human-readable display name for a quality gate type + * @param gateType - The gate type (E2E or backend naming) + * @returns Human-readable gate name + * @example + * getGateName('tests') // returns 'Tests' + * getGateName('type_check') // returns 'Type Check' + */ +export function getGateName(gateType: GateTypeE2E | string): string { + switch (gateType) { + case 'tests': + return 'Tests'; + case 'coverage': + return 'Coverage'; + case 'type-check': + case 'type_check': + return 'Type Check'; + case 'lint': + case 'linting': + return 'Linting'; + case 'review': + case 'code_review': + return 'Code Review'; + default: + return gateType; + } +} + +/** + * Get Tailwind CSS classes for status badge styling + * @param status - The quality gate status value + * @returns Tailwind CSS class string for background, text, and border colors + * @example + * getStatusClasses('passed') // returns 'bg-green-100 text-green-800 border-green-300' + * getStatusClasses('failed') // returns 'bg-red-100 text-red-800 border-red-300' + */ +export function getStatusClasses(status: QualityGateStatusValue): string { + switch (status) { + case 'passed': + return 'bg-green-100 text-green-800 border-green-300'; + case 'failed': + return 'bg-red-100 text-red-800 border-red-300'; + case 'running': + return 'bg-yellow-100 text-yellow-800 border-yellow-300'; + case 'pending': + return 'bg-gray-100 text-gray-800 border-gray-300'; + default: + return 'bg-gray-100 text-gray-800 border-gray-200'; + } +} + +/** + * Get the icon emoji for a quality gate status + * @param status - The quality gate status value + * @returns Icon emoji string representing the status + * @example + * getStatusIcon('passed') // returns 'βœ…' + * getStatusIcon('failed') // returns '❌' + * getStatusIcon('running') // returns '⏳' + */ +export function getStatusIcon(status: QualityGateStatusValue): string { + switch (status) { + case 'passed': + return 'βœ…'; + case 'failed': + return '❌'; + case 'running': + return '⏳'; + case 'pending': + return '⏸️'; + default: + return '❓'; + } +} + +/** + * Get Tailwind CSS classes for severity badge styling + * @param severity - The failure severity level (critical, high, medium, low) + * @returns Tailwind CSS class string for background, text, and border colors + * @example + * getSeverityClasses('critical') // returns 'bg-red-100 text-red-900 border-red-300' + * getSeverityClasses('high') // returns 'bg-orange-100 text-orange-900 border-orange-300' + */ +export function getSeverityClasses(severity: string): string { + switch (severity) { + case 'critical': + return 'bg-red-100 text-red-900 border-red-300'; + case 'high': + return 'bg-orange-100 text-orange-900 border-orange-300'; + case 'medium': + return 'bg-yellow-100 text-yellow-900 border-yellow-300'; + case 'low': + return 'bg-blue-100 text-blue-900 border-blue-300'; + default: + return 'bg-gray-100 text-gray-900 border-gray-300'; + } +} diff --git a/web-ui/src/types/qualityGates.ts b/web-ui/src/types/qualityGates.ts index 352011fe..e884dc37 100644 --- a/web-ui/src/types/qualityGates.ts +++ b/web-ui/src/types/qualityGates.ts @@ -4,7 +4,10 @@ */ /** - * Quality gate types that can be evaluated + * Backend naming convention for quality gates + * (used in API responses and database) + * + * Note: This is the canonical backend type. Use GateTypeBackend alias below for clarity. */ export type QualityGateType = 'tests' | 'type_check' | 'coverage' | 'code_review' | 'linting'; @@ -55,3 +58,62 @@ export interface TriggerQualityGatesResponse { status: QualityGateStatusValue; message: string; } + +/** + * E2E test naming convention for quality gates + * + * Uses kebab-case (e.g., 'type-check', 'lint') to match: + * - HTML test IDs (data-testid attributes) + * - Frontend display conventions + * - TypeScript/JavaScript naming patterns + */ +export type GateTypeE2E = 'tests' | 'coverage' | 'type-check' | 'lint' | 'review'; + +/** + * Backend naming convention for quality gates (alias of QualityGateType) + * + * Uses snake_case (e.g., 'type_check', 'linting') following: + * - Python PEP 8 naming conventions + * - Backend API response format + * - Database column naming standards + */ +export type GateTypeBackend = QualityGateType; + +/** + * Map E2E gate type to backend gate type + * @param gateType - E2E gate type (kebab-case) + * @returns Backend gate type (snake_case) + */ +export function mapE2EToBackend(gateType: GateTypeE2E): GateTypeBackend { + const mapping: Record = { + 'tests': 'tests', + 'coverage': 'coverage', + 'type-check': 'type_check', + 'lint': 'linting', + 'review': 'code_review', + }; + return mapping[gateType]; +} + +/** + * Map backend gate type to E2E gate type + * @param gateType - Backend gate type (snake_case) + * @returns E2E gate type (kebab-case) + */ +export function mapBackendToE2E(gateType: GateTypeBackend): GateTypeE2E { + const mapping: Record = { + 'tests': 'tests', + 'coverage': 'coverage', + 'type_check': 'type-check', + 'linting': 'lint', + 'code_review': 'review', + }; + return mapping[gateType]; +} + +/** + * All quality gate types in E2E naming convention + * Centralized constant to ensure consistency across components + */ +export const ALL_GATE_TYPES_E2E: readonly GateTypeE2E[] = + ['tests', 'coverage', 'type-check', 'lint', 'review'] as const;