diff --git a/frontend/AGENT.md b/frontend/AGENT.md index a0fccfa..b33b117 100644 --- a/frontend/AGENT.md +++ b/frontend/AGENT.md @@ -174,7 +174,7 @@ Important note: - Repository interaction is abstracted behind `lib/repository-handler.ts`. - Demo/mock behavior is provided through: - - `lib/mock-api.ts` + - `lib/api.ts` - `lib/demo-visualizer-fixture.ts` - Repository connection loading/error handling is currently local-state driven: - async handlers set inline `isLoading` / `error` state in `use-repository-session.ts` diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 1fdf0ea..4f6045b 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -19,7 +19,7 @@ export default function RootLayout({ }>) { return ( - {children} + {children} ); } diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index aaacb9c..813fcaa 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,15 +1,19 @@ "use client" +import Image from "next/image" import RepositorySelector from "@/screens/repository/repository-selector" import VisualizerWorkspace from "@/screens/visualizer/visualizer-workspace" +import spinnerIcon from "@/assets/spinner.png" import Header from "@/components/app/header" import { useRepositorySession } from "@/hooks/use-repository-session" export default function Home() { const { + isBootstrapping, isLoading, error, + pendingRepository, currentRepository, currentRepositoryData, showRepositorySelector, @@ -19,7 +23,7 @@ export default function Home() { handleRepositoryRefresh, } = useRepositorySession() - const isWorkspaceView = Boolean(currentRepository && !showRepositorySelector) + const isWorkspaceView = isBootstrapping || Boolean(currentRepository && !showRepositorySelector) return (
@@ -31,13 +35,19 @@ export default function Home() {
- {showRepositorySelector && ( + {isBootstrapping ? ( +
+ +
+ ) : null} + + {!isBootstrapping && showRepositorySelector && ( )} diff --git a/frontend/archive/components/common/collapsible-card.tsx b/frontend/archive/components/common/collapsible-card.tsx deleted file mode 100644 index 208923e..0000000 --- a/frontend/archive/components/common/collapsible-card.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client" - -import type React from "react" -import { ChevronDown, ChevronRight } from "lucide-react" -import { motion } from "framer-motion" -import { Badge } from "@/components/ui/badge" - -interface CollapsibleCardProps { - title: string - children: React.ReactNode - isOpen?: boolean - onToggle?: () => void - borderColor?: string - isExpanded?: boolean - badgeText?: string - badgeColor?: string - className?: string - headerClassName?: string -} - -export const CollapsibleCard: React.FC = ({ - title, - children, - isOpen = true, - onToggle, - borderColor = "border-blue-500", - isExpanded, - badgeText, - badgeColor, - className = "", - headerClassName = "", -}) => { - const colorMap: Record = { - "border-blue-500": "text-blue-600", - "border-purple-500": "text-purple-600", - "border-green-500": "text-green-600", - "border-red-500": "text-red-600", - "border-amber-500": "text-amber-600", - "border-slate-300": "text-slate-600", - "border-indigo-500": "text-indigo-600", - "border-cyan-500": "text-cyan-600", - } - - const titleColor = colorMap[borderColor] || "text-gray-700" - - return ( - -
-
-
{title}
- {badgeText && ( - - {badgeText} - - )} -
- {onToggle && ( - { - e.stopPropagation() - onToggle() - }} - className="text-gray-500 hover:text-gray-700" - aria-label={isExpanded ? "Collapse" : "Expand"} - > - {isExpanded ? : } - - )} -
- {isOpen && ( - - {children} - - )} -
- ) -} diff --git a/frontend/archive/components/common/enhanced-view-mode-toggle.tsx b/frontend/archive/components/common/enhanced-view-mode-toggle.tsx deleted file mode 100644 index 8af1e85..0000000 --- a/frontend/archive/components/common/enhanced-view-mode-toggle.tsx +++ /dev/null @@ -1,131 +0,0 @@ -"use client" - -import { Lightbulb, Cog } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Card, CardContent } from "@/components/ui/card" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import type { ViewMode } from "@/lib/view-mode-utils" - -interface EnhancedViewModeToggleProps { - viewMode: ViewMode - onViewModeChange: (mode: ViewMode) => void - hiddenCount?: number - className?: string -} - -export default function EnhancedViewModeToggle({ - viewMode, - onViewModeChange, - hiddenCount, - className = "", -}: EnhancedViewModeToggleProps) { - return ( - - -
-
-
- {viewMode === "normal" ? ( - - ) : ( - - )} -
-
-
- - {viewMode === "normal" ? "Beginner Mode" : "Advanced Mode"} - - - {viewMode === "normal" ? "Simplified" : "Technical"} - -
-

- {viewMode === "normal" - ? "Shows only security essentials - perfect for learning gittuf basics" - : "Shows all technical details - for experienced users and developers"} -

- {viewMode === "normal" && hiddenCount && hiddenCount > 0 && ( -

- ✨ {hiddenCount} technical fields hidden to keep things simple -

- )} -
-
- -
- - - - - - -
-
Beginner Mode
-
- Perfect for learning gittuf! Shows only the most important security information: -
-
    -
  • • Expiration dates and security status
  • -
  • • User roles and permissions
  • -
  • • Security policies and rules
  • -
  • • Trust relationships
  • -
-
Hides technical details
-
-
-
-
- - - - - - - -
-
Advanced Mode
-
For experienced users and developers. Shows everything including:
-
    -
  • • All security information from Beginner mode
  • -
  • • Schema versions and metadata types
  • -
  • • Key details and technical fields
  • -
  • • Raw data structures
  • -
-
Complete technical view
-
-
-
-
-
-
-
-
- ) -} diff --git a/frontend/archive/components/common/quick-start-guide.tsx b/frontend/archive/components/common/quick-start-guide.tsx deleted file mode 100644 index 0745811..0000000 --- a/frontend/archive/components/common/quick-start-guide.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client" - -import { useState } from "react" -import { ChevronDown, ChevronRight, Github, Search, Eye, GitCompare, BarChart3, HelpCircle } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" - -export default function QuickStartGuide() { - const [isOpen, setIsOpen] = useState(false) - - const steps = [ - { - number: 1, - title: "Enter Repository URL", - description: "Paste a GitHub repository URL that contains gittuf metadata", - icon: , - tip: "Try the demo button if you don't have a gittuf repository handy!", - }, - { - number: 2, - title: "Browse Commits", - description: "Explore the commit history and select commits to analyze", - icon: , - tip: "Use the different selection modes: single commit, compare two commits, or analyze multiple commits", - }, - { - number: 3, - title: "Choose View Mode", - description: "Switch between Normal (beginner-friendly) and Advanced (technical details) modes", - icon: , - tip: "Normal mode hides technical details and shows only security-critical information", - }, - { - number: 4, - title: "Visualize & Compare", - description: "Use different visualization modes to understand the security metadata", - icon: , - tip: "Tree view is great for beginners, Graph view shows relationships, Compare shows differences", - }, - { - number: 5, - title: "Analyze Trends", - description: "Select multiple commits to see how security policies evolved over time", - icon: , - tip: "Look for patterns in security changes and policy updates", - }, - ] - - return ( - - - - -
-
- - Quick Start Guide - - 5 steps - -
- {isOpen ? ( - - ) : ( - - )} -
-
-
- - -
- {steps.map((step) => ( -
-
-
- {step.number} -
-
-
-
- {step.icon} -

{step.title}

-
-

{step.description}

-

- 💡 Tip: {step.tip} -

-
-
- ))} -
-
-
-
-
- ) -} diff --git a/frontend/archive/components/common/view-mode-toggle.tsx b/frontend/archive/components/common/view-mode-toggle.tsx deleted file mode 100644 index 6fbb740..0000000 --- a/frontend/archive/components/common/view-mode-toggle.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client" - -import { Eye, EyeOff, Info } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import type { ViewMode } from "@/lib/view-mode-utils" - -interface ViewModeToggleProps { - viewMode: ViewMode - onViewModeChange: (mode: ViewMode) => void - hiddenCount?: number - className?: string -} - -export default function ViewModeToggle({ - viewMode, - onViewModeChange, - hiddenCount, - className = "", -}: ViewModeToggleProps) { - return ( -
-
- View Mode: -
- - -
-
- -
- - - -
- - - {viewMode === "normal" ? "Security essentials only" : "All technical details"} - -
-
- -
-
{viewMode === "normal" ? "Normal Mode" : "Advanced Mode"}
-
- {viewMode === "normal" - ? "Shows only critical security fields like expiration dates, thresholds, principals, roles, and policies. Hides technical implementation details." - : "Shows all fields including technical details like schema versions, key algorithms, and raw cryptographic data."} -
- {viewMode === "normal" && hiddenCount && hiddenCount > 0 && ( -
{hiddenCount} technical fields hidden
- )} -
-
-
-
- - {viewMode === "normal" && hiddenCount && hiddenCount > 0 && ( - - {hiddenCount} hidden - - )} -
-
- ) -} diff --git a/frontend/archive/components/common/welcome-screen.tsx b/frontend/archive/components/common/welcome-screen.tsx deleted file mode 100644 index 5688740..0000000 --- a/frontend/archive/components/common/welcome-screen.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import { Github, Sparkles } from "lucide-react" -import { Button } from "@/components/ui/button" - -interface WelcomeScreenProps { - onTryDemo: () => void -} - -export default function WelcomeScreen({ onTryDemo }: WelcomeScreenProps) { - return ( -
- -

Ready to Explore Security Metadata

-

- Enter a GitHub repository URL above or try our interactive demo to start learning about gittuf security policies -

- -
- ) -} diff --git a/frontend/archive/components/common/welcome-section.tsx b/frontend/archive/components/common/welcome-section.tsx deleted file mode 100644 index 5cddf42..0000000 --- a/frontend/archive/components/common/welcome-section.tsx +++ /dev/null @@ -1,148 +0,0 @@ -"use client" - -import { useState } from "react" -import { Shield, GitBranch, FileText, ChevronRight, X, Play } from "lucide-react" -import { FILENAMES } from "@/lib/constants" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { motion, AnimatePresence } from "framer-motion" - -interface WelcomeSectionProps { - onTryDemo: () => void - onDismiss: () => void -} - -export default function WelcomeSection({ onTryDemo, onDismiss }: WelcomeSectionProps) { - const [currentStep, setCurrentStep] = useState(0) - - const steps = [ - { - icon: , - title: "What is gittuf?", - description: - "gittuf is a security layer for Git repositories that provides cryptographic verification and policy enforcement independent of hosting platforms like GitHub.", - details: - "It ensures that only authorized users can make changes to your repository by using digital signatures and security policies.", - }, - { - icon: , - title: "Security Metadata", - description: - `gittuf stores security policies in JSON files like ${FILENAMES.ROOT} and ${FILENAMES.TARGETS} that define who can do what in your repository.`, - details: "These files contain cryptographic keys, access rules, and expiration dates that protect your code.", - }, - { - icon: , - title: "Version History", - description: - "This tool helps you visualize how your security policies evolve over time by comparing different commits.", - details: - "You can see what changed, when it changed, and understand the security implications of each modification.", - }, - ] - - return ( - - - -
-
-
- -
-
- Welcome to gittuf Metadata Visualizer -

- Learn about Git repository security through interactive visualization -

-
-
- -
-
- - {/* Step indicator */} -
- {steps.map((_, index) => ( -
-
- {index < steps.length - 1 && ( -
- )} -
- ))} -
- - {/* Current step content */} - - -
{steps[currentStep].icon}
-
-

{steps[currentStep].title}

-

{steps[currentStep].description}

-

{steps[currentStep].details}

-
-
-
- - {/* Navigation buttons */} -
- - -
- - - {currentStep < steps.length - 1 ? ( - - ) : ( - - )} -
-
- - - - ) -} diff --git a/frontend/archive/page-components/commit/commit-analysis.tsx b/frontend/archive/page-components/commit/commit-analysis.tsx deleted file mode 100644 index 93938a8..0000000 --- a/frontend/archive/page-components/commit/commit-analysis.tsx +++ /dev/null @@ -1,511 +0,0 @@ -"use client" - -import { useState, useMemo } from "react" -import { - Loader2, - TrendingUp, - GitCommit, - Calendar, - Shield, - AlertTriangle, - CheckCircle, - Clock, -} from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Badge } from "@/components/ui/badge" -import { Progress } from "@/components/ui/progress" -import { compareJsonObjects, type DiffResult, type DiffEntry } from "@/lib/json-diff" -import type { Commit, SecurityEvent, SecurityTrend } from "@/lib/types" -import { motion } from "framer-motion" -import { SecurityInsights } from "./security-insights" -import { SecurityRecommendations } from "./security-recommendations" - -interface CommitAnalysisProps { - commits: Commit[] - isLoading: boolean - selectedFile: string -} - -export default function CommitAnalysis({ commits, isLoading, selectedFile }: CommitAnalysisProps) { - const [activeTab, setActiveTab] = useState("timeline") - const [error] = useState(null) - - const analyzeSecurityEvents = (diff: DiffResult | DiffEntry | null, commit: Commit): SecurityEvent[] => { - const events: SecurityEvent[] = [] - - // If diff is null or singular DiffEntry (root change), we might need to handle it. - // Assuming structure is mostly Record for traversal. - - const traverseChanges = (obj: Record | undefined, path = "") => { - if (!obj) return - - Object.entries(obj).forEach(([key, value]) => { - const currentPath = path ? `${path}.${key}` : key - const pathLower = currentPath.toLowerCase() - - if (value.status === "added" || value.status === "removed" || value.status === "changed") { - let event: SecurityEvent | null = null - - // Expiration changes - if (pathLower.includes("expires")) { - if (value.status === "changed") { - const oldDate = new Date(String(value.oldValue)) - const newDate = new Date(String(value.value)) - const extended = newDate > oldDate - - event = { - commit: commit.hash.substring(0, 8), - date: commit.date, - author: commit.author, - message: commit.message, - type: "expiration_change", - severity: extended ? "medium" : "high", - description: extended ? "Security validity extended" : "Security validity shortened", - details: `Expiration ${extended ? "extended" : "shortened"} from ${oldDate.toLocaleDateString()} to ${newDate.toLocaleDateString()}`, - impact: extended - ? "Positive: More time before renewal required" - : "Negative: Earlier renewal required, potential service disruption risk", - } - } - } - - // Threshold changes - else if (pathLower.includes("threshold")) { - if (value.status === "changed" && typeof value.value === 'number' && typeof value.oldValue === 'number') { - const increased = value.value > value.oldValue - event = { - commit: commit.hash.substring(0, 8), - date: commit.date, - author: commit.author, - message: commit.message, - type: increased ? "security_enhancement" : "security_degradation", - severity: increased ? "medium" : "high", - description: `Security threshold ${increased ? "increased" : "decreased"}`, - details: `Threshold changed from ${value.oldValue} to ${value.value} required signatures`, - impact: increased - ? "Positive: Enhanced security, requires more approvals" - : "Negative: Reduced security, fewer approvals needed", - } - } - } - - // Principal changes - else if (pathLower.includes("principals") || pathLower.includes("principalids")) { - event = { - commit: commit.hash.substring(0, 8), - date: commit.date, - author: commit.author, - message: commit.message, - type: "principal_change", - severity: value.status === "removed" ? "high" : "medium", - description: `Security principal ${value.status}`, - details: `Principal access ${value.status === "added" ? "granted" : value.status === "removed" ? "revoked" : "modified"}`, - impact: - value.status === "added" - ? "Neutral: New authorized user added" - : value.status === "removed" - ? "Negative: User access revoked, may impact operations" - : "Neutral: User permissions modified", - } - } - - // Trust changes - else if (pathLower.includes("trusted")) { - if (value.status === "changed") { - const nowTrusted = value.value === true - event = { - commit: commit.hash.substring(0, 8), - date: commit.date, - author: commit.author, - message: commit.message, - type: nowTrusted ? "security_enhancement" : "security_degradation", - severity: "medium", - description: `Component trust ${nowTrusted ? "granted" : "revoked"}`, - details: `Trust status changed to ${value.value}`, - impact: nowTrusted - ? "Positive: Component granted special privileges" - : "Negative: Component privileges revoked", - } - } - } - - // Rule changes - else if (pathLower.includes("rules")) { - event = { - commit: commit.hash.substring(0, 8), - date: commit.date, - author: commit.author, - message: commit.message, - type: "policy_change", - severity: value.status === "removed" ? "high" : "medium", - description: `Security rule ${value.status}`, - details: `Protection rule ${value.status === "added" ? "created" : value.status === "removed" ? "removed" : "modified"}`, - impact: - value.status === "added" - ? "Positive: New protection rule added" - : value.status === "removed" - ? "Negative: Protection rule removed, reduced security" - : "Neutral: Protection rule updated", - } - } - - if (event) { - events.push(event) - } - } - - if (value.children) { - traverseChanges(value.children, currentPath) - } - }) - } - - if (diff && !('status' in diff)) { - traverseChanges(diff as DiffResult) - } else if (diff && 'status' in diff && (diff as DiffEntry).children) { - // If root is a DiffEntry with children - traverseChanges((diff as DiffEntry).children) - } - - return events - } - - - - const calculateSecurityTrends = (commits: Commit[]): SecurityTrend[] => { - const trends: SecurityTrend[] = [] - - if (commits.length < 2) return trends - - const firstCommit = commits[0] - const lastCommit = commits[commits.length - 1] - - // Principal count trend - const firstPrincipals = firstCommit.data?.principals && typeof firstCommit.data.principals === 'object' && !Array.isArray(firstCommit.data.principals) ? Object.keys(firstCommit.data.principals).length : 0 - const lastPrincipals = lastCommit.data?.principals && typeof lastCommit.data.principals === 'object' && !Array.isArray(lastCommit.data.principals) ? Object.keys(lastCommit.data.principals).length : 0 - - trends.push({ - metric: "Security Principals", - trend: lastPrincipals > firstPrincipals ? "improving" : lastPrincipals < firstPrincipals ? "declining" : "stable", - current: lastPrincipals, - previous: firstPrincipals, - description: "Number of authorized security principals", - }) - - // Rules count trend - const firstRules = firstCommit.data?.rules && typeof firstCommit.data.rules === 'object' && !Array.isArray(firstCommit.data.rules) ? Object.keys(firstCommit.data.rules).length : 0 - const lastRules = lastCommit.data?.rules && typeof lastCommit.data.rules === 'object' && !Array.isArray(lastCommit.data.rules) ? Object.keys(lastCommit.data.rules).length : 0 - - trends.push({ - metric: "Security Rules", - trend: lastRules > firstRules ? "improving" : lastRules < firstRules ? "declining" : "stable", - current: lastRules, - previous: firstRules, - description: "Number of active security rules", - }) - - // Expiration health - if (lastCommit.data?.expires && typeof lastCommit.data.expires === 'string') { - const expiryDate = new Date(lastCommit.data.expires) - const now = new Date() - const daysUntilExpiry = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) - - trends.push({ - metric: "Expiration Health", - trend: daysUntilExpiry > 90 ? "improving" : daysUntilExpiry > 30 ? "stable" : "declining", - current: daysUntilExpiry, - previous: 0, // We don't track historical expiration health - description: "Days until security metadata expires", - }) - } - - return trends - } - - // Process commits data for comprehensive analysis using useMemo - const { securityEvents, securityTrends } = useMemo(() => { - if (!commits || commits.length < 2) { - return { securityEvents: [] as SecurityEvent[], securityTrends: [] as SecurityTrend[] } - } - - try { - // Sort commits by date - const sortedCommits = [...commits].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) - - const events: SecurityEvent[] = [] - const trends: SecurityTrend[] = [] - - // Analyze each commit transition - for (let i = 1; i < sortedCommits.length; i++) { - const prevCommit = sortedCommits[i - 1] - const currentCommit = sortedCommits[i] - - if (prevCommit.data && currentCommit.data) { - const diff = compareJsonObjects(prevCommit.data, currentCommit.data) - const commitEvents = analyzeSecurityEvents(diff, currentCommit) - events.push(...commitEvents) - } - } - - // Calculate trends - const calculatedTrends = calculateSecurityTrends(sortedCommits) - trends.push(...calculatedTrends) - - return { securityEvents: events, securityTrends: trends } - } catch (err) { - console.error("Error processing analysis data:", err) - return { securityEvents: [] as SecurityEvent[], securityTrends: [] as SecurityTrend[] } - } - }, [commits]) - - - - - - return ( -
- {/* Security Overview Dashboard */} - - -
-
-
- -
-
- Security Analysis Dashboard -

- Analyzing {commits.length} commits across {selectedFile} -

-
-
-
-
- -
-
-
- - Commits Analyzed -
-
{commits.length}
-
-
-
- - Security Events -
-
{securityEvents.length}
-
-
-
- - Active Trends -
-
{securityTrends.length}
-
-
-
- - Time Span -
-
- {commits.length > 1 - ? `${Math.ceil((new Date(commits[commits.length - 1].date).getTime() - new Date(commits[0].date).getTime()) / (1000 * 60 * 60 * 24))} days` - : "N/A"} -
-
-
-
-
- - - - - Security Timeline - - - Trends Analysis - - - Security Insights - - - Recommendations - - - - - - - - - Security Events Timeline - - - - {isLoading ? ( -
- - Analyzing security events... -
- ) : error ? ( -
- -

{error}

-
- ) : securityEvents.length > 0 ? ( -
- {securityEvents.map((event, index) => ( - -
-
-
- - {event.commit} - - - {event.severity} - - - {new Date(event.date).toLocaleDateString()} by {event.author} - -
-

{event.description}

-

{event.details}

-
-

- Impact: {event.impact} -

-
-
-
-
- ))} -
- ) : ( -
- -

No significant security events detected

-
- )} -
-
-
- - - - - - - Security Trends Analysis - - - - {isLoading ? ( -
- - Analyzing security trends... -
- ) : securityTrends.length > 0 ? ( -
- {securityTrends.map((trend, index) => ( - -
-

{trend.metric}

- - {trend.trend} - -
-
-
{trend.current}
- {trend.previous !== undefined && ( -
from {trend.previous}
- )} -
-

{trend.description}

- {trend.metric === "Expiration Health" && ( -
- -

- {trend.current > 0 ? `${trend.current} days remaining` : "Expired"} -

-
- )} -
- ))} -
- ) : ( -
- -

No trends available with current data

-
- )} -
-
-
- - - - - - - - -
-
- ) -} diff --git a/frontend/archive/page-components/commit/commit-compare.tsx b/frontend/archive/page-components/commit/commit-compare.tsx deleted file mode 100644 index ebe76ee..0000000 --- a/frontend/archive/page-components/commit/commit-compare.tsx +++ /dev/null @@ -1,773 +0,0 @@ -"use client" - -import { Loader2, GitCompare, AlertTriangle, Minus, Plus, Edit3 } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import JsonDiffVisualization from "@/legacy/page-components/json/json-diff-visualization" -import JsonDiffStats from "@/legacy/page-components/json/json-diff-stats" -import { Button } from "@/components/ui/button" -import type { Commit, JsonObject, JsonValue } from "@/lib/types" -import { useState } from "react" -import JsonTreeView from "@/legacy/page-components/json/json-tree-view" -import { compareJsonObjects, countChanges, type DiffResult, type DiffEntry } from "@/lib/json-diff" -import type { ViewMode } from "@/lib/view-mode-utils" -import { motion } from "framer-motion" - -interface CommitCompareProps { - baseCommit: Commit - compareCommit: Commit - baseData: JsonObject | null - compareData: JsonObject | null - isLoading: boolean - selectedFile: string - viewMode?: ViewMode -} - -export default function CommitCompare({ - baseCommit, - compareCommit, - baseData, - compareData, - isLoading, - selectedFile, - viewMode = "advanced", -}: CommitCompareProps) { - const [error, setError] = useState(null) - - // Calculate diff statistics - const diff = baseData && compareData ? compareJsonObjects(baseData, compareData) : null - const { added, removed, changed, unchanged } = diff - ? countChanges(diff) - : { added: 0, removed: 0, changed: 0, unchanged: 0 } - const totalChanges = added + removed + changed - - // Get time difference between commits - const getTimeDifference = () => { - const baseDate = new Date(baseCommit.date) - const compareDate = new Date(compareCommit.date) - const diffMs = Math.abs(compareDate.getTime() - baseDate.getTime()) - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) - const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) - - if (diffDays > 0) { - return `${diffDays} day${diffDays > 1 ? "s" : ""} apart` - } else if (diffHours > 0) { - return `${diffHours} hour${diffHours > 1 ? "s" : ""} apart` - } else { - return "Less than an hour apart" - } - } - - // Get security impact assessment - const getSecurityImpact = () => { - if (!diff) return null - - let criticalChanges = 0 - let warningChanges = 0 - let infoChanges = 0 - - const assessChange = (path: string) => { - const pathLower = path.toLowerCase() - - // Critical security changes - if (pathLower.includes("expire") || pathLower.includes("threshold") || pathLower.includes("trusted")) { - criticalChanges++ - } - // Warning level changes - else if (pathLower.includes("principal") || pathLower.includes("role") || pathLower.includes("rule")) { - warningChanges++ - } - // Info level changes - else { - infoChanges++ - } - } - - const traverseChanges = (obj: DiffResult | null, path = "") => { - if (!obj) return - - Object.entries(obj).forEach(([key, entry]: [string, DiffEntry]) => { - const currentPath = path ? `${path}.${key}` : key - - if (entry.status === "added" || entry.status === "removed" || entry.status === "changed") { - assessChange(currentPath) - } - - if (entry.children) { - traverseChanges(entry.children, currentPath) - } - }) - } - - // Handle case where diff might be a single DiffEntry (from null checks in compareJsonObjects) - if ('status' in diff && typeof diff.status === 'string') { - const entry = diff as DiffEntry - if (entry.status === "added" || entry.status === "removed" || entry.status === "changed") { - assessChange("root") - } - if (entry.children) { - traverseChanges(entry.children, "") - } - } else { - traverseChanges(diff as DiffResult, "") - } - - return { criticalChanges, warningChanges, infoChanges } - } - - const securityImpact = getSecurityImpact() - - return ( -
- {/* Enhanced Comparison Header */} - - -
-
-
- -
-
- Security Metadata Comparison -

- Analyzing changes in {selectedFile} • {getTimeDifference()} -

-
-
- {totalChanges > 0 && ( -
- - {totalChanges} change{totalChanges !== 1 ? "s" : ""} - - {securityImpact && securityImpact.criticalChanges > 0 && ( - - {securityImpact.criticalChanges} critical - - )} -
- )} -
-
- - {/* Commit Comparison Cards */} -
- -
- - Base Commit - - - {baseCommit.hash.substring(0, 8)} - -
-

{baseCommit.message}

-
- {baseCommit.author} - {new Date(baseCommit.date).toLocaleDateString()} -
-
- - -
- - Compare Commit - - - {compareCommit.hash.substring(0, 8)} - -
-

{compareCommit.message}

-
- {compareCommit.author} - {new Date(compareCommit.date).toLocaleDateString()} -
-
-
- - {/* Quick Stats */} - {totalChanges > 0 && ( - -
-
- - {added} -
-

Added

-
-
-
- - {removed} -
-

Removed

-
-
-
- - {changed} -
-

Modified

-
-
-
- {unchanged} -
-

Unchanged

-
-
- )} - - {/* Security Impact Assessment */} - {securityImpact && (securityImpact.criticalChanges > 0 || securityImpact.warningChanges > 0) && ( - -

- - Security Impact Assessment -

-
- {securityImpact.criticalChanges > 0 && ( -
-
- - {securityImpact.criticalChanges} critical security change - {securityImpact.criticalChanges !== 1 ? "s" : ""} - -
- )} - {securityImpact.warningChanges > 0 && ( -
-
- - {securityImpact.warningChanges} policy change{securityImpact.warningChanges !== 1 ? "s" : ""} - -
- )} - {securityImpact.infoChanges > 0 && ( -
-
- - {securityImpact.infoChanges} other change{securityImpact.infoChanges !== 1 ? "s" : ""} - -
- )} -
-
- )} -
-
- - - - - Visual Diff - - - Change Analysis - - - Side-by-Side - - - Change Timeline - - - - -
- {isLoading ? ( -
- - Loading security metadata comparison... -
- ) : error ? ( -
- -

{error}

- -
- ) : baseData && compareData ? ( - - ) : ( -
- -

Select two commits to compare security metadata

-
- )} -
-
- - -
- {isLoading ? ( -
- - Loading security metadata comparison... -
- ) : error ? ( -
- -

{error}

- -
- ) : baseData && compareData ? ( - - ) : ( -
- -

Select two commits to compare security metadata

-
- )} -
-
- - -
- {isLoading ? ( -
- - Loading tree comparison... -
- ) : error ? ( -
- -

{error}

- -
- ) : baseData && compareData ? ( -
-
-
- - Base - - {baseCommit.hash.substring(0, 8)} - - {new Date(baseCommit.date).toLocaleDateString()} - -
-
- -
-
-
-
- - Compare - - {compareCommit.hash.substring(0, 8)} - - {new Date(compareCommit.date).toLocaleDateString()} - -
-
- -
-
-
- ) : ( -
- -

Select two commits to compare in tree view

-
- )} -
-
- - - - -
-
- ) -} - -// New component for change timeline -function ChangeTimeline({ - baseCommit, - compareCommit, - baseData, - compareData, - isLoading, -}: { - baseCommit: Commit - compareCommit: Commit - baseData: JsonObject | null - compareData: JsonObject | null - isLoading: boolean -}) { - if (isLoading) { - return ( -
- - Loading change timeline... -
- ) - } - - if (!baseData || !compareData) { - return ( -
- -

Select two commits to view change timeline

-
- ) - } - - const diff = compareJsonObjects(baseData, compareData) - const changes: Array<{ - path: string - type: "added" | "removed" | "changed" - oldValue?: JsonValue - newValue?: JsonValue - impact: "critical" | "warning" | "info" - }> = [] - - const getImpactLevel = (path: string): "critical" | "warning" | "info" => { - const pathLower = path.toLowerCase() - if (pathLower.includes("expire") || pathLower.includes("threshold") || pathLower.includes("trusted")) { - return "critical" - } else if (pathLower.includes("principal") || pathLower.includes("role") || pathLower.includes("rule")) { - return "warning" - } - return "info" - } - - const traverseChanges = (obj: DiffResult | null, path = "") => { - if (!obj) return - - Object.entries(obj).forEach(([key, entry]: [string, DiffEntry]) => { - const currentPath = path ? `${path}.${key}` : key - - if (entry.status === "added") { - changes.push({ - path: currentPath, - type: "added", - newValue: entry.value, - impact: getImpactLevel(currentPath), - }) - } else if (entry.status === "removed") { - changes.push({ - path: currentPath, - type: "removed", - oldValue: entry.value, - impact: getImpactLevel(currentPath), - }) - } else if (entry.status === "changed") { - changes.push({ - path: currentPath, - type: "changed", - oldValue: entry.oldValue, - newValue: entry.value, - impact: getImpactLevel(currentPath), - }) - } - - if (entry.children) { - traverseChanges(entry.children, currentPath) - } - }) - } - - // Handle case where diff might be a single DiffEntry - if (diff && 'status' in diff && typeof diff.status === 'string') { - const entry = diff as DiffEntry - if (entry.status === "added") { - changes.push({ path: "root", type: "added", newValue: entry.value, impact: "critical" }) - } else if (entry.status === "removed") { - changes.push({ path: "root", type: "removed", oldValue: entry.value, impact: "critical" }) - } - if (entry.children) { - traverseChanges(entry.children, "") - } - } else { - traverseChanges(diff as DiffResult, "") - } - - // Sort changes by impact level - const sortedChanges = changes.sort((a, b) => { - const impactOrder = { critical: 0, warning: 1, info: 2 } - return impactOrder[a.impact] - impactOrder[b.impact] - }) - - // Enhanced change analysis with outcome descriptions - const getChangeOutcome = (change: { path: string; type: string; oldValue?: JsonValue; newValue?: JsonValue }): string => { - const pathLower = change.path.toLowerCase() - - if (pathLower.includes("expires")) { - if (change.type === "changed" && change.oldValue && change.newValue) { - const oldDate = new Date(String(change.oldValue)) - const newDate = new Date(String(change.newValue)) - const extended = newDate > oldDate - return extended - ? `Security metadata validity extended until ${newDate.toLocaleDateString()}. This gives more time before renewal is required.` - : `Security metadata validity shortened to ${newDate.toLocaleDateString()}. Renewal will be required sooner.` - } - return change.type === "added" && change.newValue - ? `New expiration date set. Security metadata will need renewal by ${new Date(String(change.newValue)).toLocaleDateString()}.` - : "Expiration date removed. This may indicate a configuration error or security risk." - } - - if (pathLower.includes("threshold")) { - if (change.type === "changed" && change.oldValue !== undefined && change.newValue !== undefined) { - const oldVal = Number(change.oldValue) - const newVal = Number(change.newValue) - const increased = newVal > oldVal - return increased - ? `Security threshold increased from ${oldVal} to ${newVal} signatures. This makes the repository more secure but requires more approvals.` - : `Security threshold decreased from ${oldVal} to ${newVal} signatures. This makes operations easier but potentially less secure.` - } - return change.type === "added" && change.newValue !== undefined - ? `New security threshold of ${change.newValue} signature(s) required. Operations now need multiple approvals.` - : "Security threshold removed. This may allow unauthorized operations." - } - - if (pathLower.includes("principals")) { - if (change.type === "added") { - return "New security principal added. This person/key can now sign and validate repository operations." - } - if (change.type === "removed") { - return "Security principal removed. This person/key can no longer sign or validate operations." - } - return "Security principal modified. The cryptographic identity or permissions have been updated." - } - - if (pathLower.includes("roles")) { - if (change.type === "added") { - return "New access role created. This defines a new set of permissions and responsibilities." - } - if (change.type === "removed") { - return "Access role removed. Users previously assigned to this role may lose permissions." - } - return "Access role modified. The permissions or assigned principals have been updated." - } - - if (pathLower.includes("rules")) { - if (change.type === "added") { - return "New security rule added. This creates additional protection for specific repository paths or actions." - } - if (change.type === "removed") { - return "Security rule removed. Protection for certain paths or actions has been lifted." - } - return "Security rule modified. The protection scope or requirements have been updated." - } - - if (pathLower.includes("trusted")) { - if (change.type === "changed") { - return change.newValue - ? "Component marked as trusted. This grants it special privileges in the security framework." - : "Component trust revoked. It no longer has special privileges and may be restricted." - } - return change.type === "added" - ? "New trusted component added. This grants special security privileges." - : "Trusted component removed. Special privileges have been revoked." - } - - if (pathLower.includes("github")) { - if (change.type === "added") { - return "GitHub App integration added. This allows automated operations through GitHub workflows." - } - if (change.type === "removed") { - return "GitHub App integration removed. Automated operations through GitHub are no longer possible." - } - return "GitHub App integration modified. The permissions or configuration have been updated." - } - - if (pathLower.includes("pattern")) { - return change.type === "added" - ? `New protection pattern "${change.newValue}" added. This defines which repository paths are protected.` - : change.type === "removed" - ? `Protection pattern "${change.oldValue}" removed. These paths are no longer protected.` - : `Protection pattern changed from "${change.oldValue}" to "${change.newValue}". Different paths are now protected.` - } - - if (pathLower.includes("action")) { - return change.type === "added" - ? `New protected action "${change.newValue}" defined. This operation now requires authorization.` - : change.type === "removed" - ? `Protected action "${change.oldValue}" removed. This operation no longer requires authorization.` - : `Protected action changed from "${change.oldValue}" to "${change.newValue}". Different operations are now controlled.` - } - - if (pathLower.includes("identity")) { - return change.type === "added" - ? `New identity "${change.newValue}" added. This person can now be authenticated for repository operations.` - : change.type === "removed" - ? `Identity "${change.oldValue}" removed. This person can no longer be authenticated.` - : `Identity changed from "${change.oldValue}" to "${change.newValue}". The authentication details have been updated.` - } - - if (pathLower.includes("keytype")) { - return change.type === "changed" - ? `Key algorithm changed from ${change.oldValue} to ${change.newValue}. This affects how signatures are created and verified.` - : change.type === "added" - ? `Key algorithm ${change.newValue} specified. This determines the cryptographic method used.` - : "Key algorithm specification removed. This may cause signature verification issues." - } - - if (pathLower.includes("schemaversion")) { - return change.type === "changed" - ? `Schema version updated from ${change.oldValue} to ${change.newValue}. The metadata structure has been modernized.` - : "Schema version specification updated. This affects how the metadata is interpreted." - } - - // Generic outcomes based on change type - switch (change.type) { - case "added": - return "New configuration element added. This extends the security framework with additional settings." - case "removed": - return "Configuration element removed. This simplifies the setup but may reduce security coverage." - case "changed": - return "Configuration element modified. The security behavior has been adjusted." - default: - return "Security configuration updated." - } - } - - return ( -
-
-

Change Timeline

-
- From - {baseCommit.hash.substring(0, 8)} - to - {compareCommit.hash.substring(0, 8)} -
-
- - {sortedChanges.length === 0 ? ( -
-

No changes detected between these commits

-
- ) : ( -
- {sortedChanges.map((change, index) => ( - -
-
-
- {change.type === "added" ? ( - - ) : change.type === "removed" ? ( - - ) : ( - - )} - {change.path} - - {change.impact} - -
- -
- {change.type === "changed" && ( -
-

Modified field

-
-
-

Before:

- {JSON.stringify(change.oldValue, null, 2)} -
-
-

After:

- {JSON.stringify(change.newValue, null, 2)} -
-
- {/* Add outcome description */} -
-

Outcome:

-

{getChangeOutcome(change)}

-
-
- )} - - {change.type === "added" && ( -
-

Added new field

-
- {JSON.stringify(change.newValue, null, 2)} -
- {/* Add outcome description */} -
-

Outcome:

-

{getChangeOutcome(change)}

-
-
- )} - - {change.type === "removed" && ( -
-

Removed field

-
- {JSON.stringify(change.oldValue, null, 2)} -
- {/* Add outcome description */} -
-

Outcome:

-

{getChangeOutcome(change)}

-
-
- )} -
-
-
-
- ))} -
- )} -
- ) -} diff --git a/frontend/archive/page-components/commit/commit-list.tsx b/frontend/archive/page-components/commit/commit-list.tsx deleted file mode 100644 index 9e48430..0000000 --- a/frontend/archive/page-components/commit/commit-list.tsx +++ /dev/null @@ -1,296 +0,0 @@ -"use client" - -import { useState } from "react" -import { motion } from "framer-motion" -import { GitCommit, ChevronRight, Search, GitCompare, BarChart3 } from "lucide-react" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { Checkbox } from "@/components/ui/checkbox" -import { Badge } from "@/components/ui/badge" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import type { Commit } from "@/lib/types" - -interface CommitListProps { - commits: Commit[] - onSelectCommit: (commit: Commit) => void - selectedCommit: Commit | null - onCompareSelect: (base: Commit, compare: Commit) => void - compareCommits: { base: Commit | null; compare: Commit | null } - onRangeSelect: (commits: Commit[]) => void -} - -export default function CommitList({ - commits, - onSelectCommit, - selectedCommit, - onCompareSelect, - compareCommits, - onRangeSelect, -}: CommitListProps) { - const [searchTerm, setSearchTerm] = useState("") - const [selectionMode, setSelectionMode] = useState<"single" | "compare" | "range">("single") - const [selectedCommits, setSelectedCommits] = useState>({}) - - const filteredCommits = commits.filter( - (commit) => - commit.message.toLowerCase().includes(searchTerm.toLowerCase()) || - commit.hash.toLowerCase().includes(searchTerm.toLowerCase()) || - commit.author.toLowerCase().includes(searchTerm.toLowerCase()), - ) - - const handleCommitClick = (commit: Commit) => { - if (selectionMode === "single") { - onSelectCommit(commit) - } else if (selectionMode === "compare") { - if (!compareCommits.base) { - onCompareSelect(commit, compareCommits.compare || commits[0]) - } else if (!compareCommits.compare) { - onCompareSelect(compareCommits.base, commit) - } else { - // If both are already selected, replace the compare commit - onCompareSelect(compareCommits.base, commit) - } - } else if (selectionMode === "range") { - // Toggle selection for range mode - setSelectedCommits((prev) => ({ - ...prev, - [commit.hash]: !prev[commit.hash], - })) - } - } - - const handleApplyRange = () => { - const selected = commits.filter((commit) => selectedCommits[commit.hash]) - if (selected.length >= 2) { - onRangeSelect(selected) - } - } - - const resetSelection = () => { - if (selectionMode === "compare") { - onCompareSelect(null as unknown as Commit, null as unknown as Commit) - } else if (selectionMode === "range") { - setSelectedCommits({}) - } - } - - return ( -
-
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
- -
- - - - - - -

Single commit selection

-
-
-
- - - - - - - -

Compare two commits

-
-
-
- - - - - - - -

Select multiple commits for analysis

-
-
-
-
-
- - {selectionMode === "compare" && ( -
-
-

Base:

- {compareCommits.base ? ( -
- - {compareCommits.base.hash.substring(0, 8)} - - {compareCommits.base.message} -
- ) : ( - Select a base commit - )} -
-
-

Compare:

- {compareCommits.compare ? ( -
- - {compareCommits.compare.hash.substring(0, 8)} - - {compareCommits.compare.message} -
- ) : ( - Select a compare commit - )} -
-
- )} - - {selectionMode === "range" && ( -
-
-

- Select multiple commits for security metadata analysis -

- - {Object.values(selectedCommits).filter(Boolean).length} selected - -
-
- - -
-
- )} - -
- {filteredCommits.length > 0 ? ( - filteredCommits.map((commit) => ( - handleCommitClick(commit)} - className={`p-3 rounded-lg border cursor-pointer transition-colors ${ - (selectionMode === "single" && selectedCommit?.hash === commit.hash) || - (selectionMode === "compare" && - (compareCommits.base?.hash === commit.hash || compareCommits.compare?.hash === commit.hash)) || - (selectionMode === "range" && selectedCommits[commit.hash]) - ? "bg-blue-50 border-blue-200" - : "bg-white border-slate-200 hover:border-slate-300" - }`} - > -
- {selectionMode === "range" && ( -
- { - setSelectedCommits((prev) => ({ - ...prev, - [commit.hash]: !prev[commit.hash], - })) - }} - onClick={(e) => e.stopPropagation()} - /> -
- )} - -
-
- -
-
-

{commit.message}

-
- - {commit.hash.substring(0, 8)} - - - {commit.author} • {new Date(commit.date).toLocaleDateString()} - -
-
-
- - {selectionMode === "compare" && ( -
- {compareCommits.base?.hash === commit.hash && ( - - Base - - )} - {compareCommits.compare?.hash === commit.hash && ( - - Compare - - )} -
- )} - - {selectionMode === "single" && ( - - )} -
-
- )) - ) : ( -
-

No commits match your search

-
- )} -
-
- ) -} diff --git a/frontend/archive/page-components/commit/security-insights.tsx b/frontend/archive/page-components/commit/security-insights.tsx deleted file mode 100644 index 9f38287..0000000 --- a/frontend/archive/page-components/commit/security-insights.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client" - -import { Target, Loader2 } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { motion } from "framer-motion" -import type { Commit, SecurityEvent } from "@/lib/types" - -interface SecurityInsightsProps { - commits: Commit[] - securityEvents: SecurityEvent[] - isLoading: boolean -} - -export function SecurityInsights({ - commits, - securityEvents, - isLoading, -}: SecurityInsightsProps) { - const getInsights = () => { - const insights = [] - - // Most active author - const authorCounts = commits.reduce( - (acc, commit) => { - acc[commit.author] = (acc[commit.author] || 0) + 1 - return acc - }, - {} as Record, - ) - - const mostActiveAuthor = Object.entries(authorCounts).sort(([, a], [, b]) => b - a)[0] - - if (mostActiveAuthor) { - insights.push({ - title: "Most Active Contributor", - description: `${mostActiveAuthor[0]} made ${mostActiveAuthor[1]} security-related commits`, - type: "info" as const, - }) - } - - // Security event patterns - const eventTypes = securityEvents.reduce( - (acc, event) => { - acc[event.type] = (acc[event.type] || 0) + 1 - return acc - }, - {} as Record, - ) - - const mostCommonEvent = Object.entries(eventTypes).sort(([, a], [, b]) => b - a)[0] - - if (mostCommonEvent) { - insights.push({ - title: "Most Common Security Change", - description: `${mostCommonEvent[1]} ${mostCommonEvent[0].replace("_", " ")} events detected`, - type: "warning" as const, - }) - } - - // Critical events - const criticalEvents = securityEvents.filter((e) => e.severity === "critical") - if (criticalEvents.length > 0) { - insights.push({ - title: "Critical Security Events", - description: `${criticalEvents.length} critical security events require immediate attention`, - type: "error" as const, - }) - } - - return insights - } - - const insights = getInsights() - - return ( - - - - - Security Insights - - - - {isLoading ? ( -
- - Generating insights... -
- ) : ( -
- {insights.map((insight, index) => ( - -

{insight.title}

-

{insight.description}

-
- ))} -
- )} -
-
- ) -} diff --git a/frontend/archive/page-components/commit/security-recommendations.tsx b/frontend/archive/page-components/commit/security-recommendations.tsx deleted file mode 100644 index 85dd5f8..0000000 --- a/frontend/archive/page-components/commit/security-recommendations.tsx +++ /dev/null @@ -1,169 +0,0 @@ -"use client" - -import { AlertTriangle, CheckCircle, Loader2 } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { motion } from "framer-motion" -import type { Commit, SecurityEvent, SecurityTrend, JsonValue } from "@/lib/types" - -interface SecurityRecommendationsProps { - commits: Commit[] - securityEvents: SecurityEvent[] - securityTrends: SecurityTrend[] - isLoading: boolean -} - -interface Recommendation { - priority: "critical" | "high" | "medium" | "low" - title: string - description: string - action: string -} - -export function SecurityRecommendations({ - commits, - securityEvents, - securityTrends, - isLoading, -}: SecurityRecommendationsProps) { - - const getRecommendations = (): Recommendation[] => { - const recommendations: Recommendation[] = [] - - // Check expiration - const latestCommit = commits[commits.length - 1] - if (latestCommit?.data?.expires && typeof latestCommit.data.expires === 'string') { - const expiryDate = new Date(latestCommit.data.expires) - const now = new Date() - const daysUntilExpiry = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) - - if (daysUntilExpiry < 30) { - recommendations.push({ - priority: "high", - title: "Renew Security Metadata", - description: `Security metadata expires in ${daysUntilExpiry} days. Plan renewal soon.`, - action: "Update expiration date and refresh security keys", - }) - } - } - - // Check thresholds - if (latestCommit?.data?.roles && typeof latestCommit.data.roles === 'object') { - Object.entries(latestCommit.data.roles).forEach(([role, config]) => { - if (config && typeof config === 'object' && !Array.isArray(config)) { - const configObj = config as Record - if (configObj.threshold === 1) { - recommendations.push({ - priority: "medium", - title: "Increase Security Threshold", - description: `Role "${role}" only requires 1 signature. Consider increasing for better security.`, - action: "Increase threshold to 2 or more signatures", - }) - } - } - }) - } - - // Check for declining trends - securityTrends.forEach((trend) => { - if (trend.trend === "declining") { - recommendations.push({ - priority: "medium", - title: `Address Declining ${trend.metric}`, - description: `${trend.metric} has decreased from ${trend.previous} to ${trend.current}.`, - action: "Review and restore security measures", - }) - } - }) - - // Critical events - const criticalEvents = securityEvents.filter((e) => e.severity === "critical") - if (criticalEvents.length > 0) { - recommendations.push({ - priority: "critical", - title: "Address Critical Security Events", - description: `${criticalEvents.length} critical security events detected.`, - action: "Review and remediate all critical security changes", - }) - } - - return recommendations.sort((a, b) => { - const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 } - return priorityOrder[a.priority] - priorityOrder[b.priority] - }) - } - - const recommendations = getRecommendations() - - return ( - - - - - Security Recommendations - - - - {isLoading ? ( -
- - Generating recommendations... -
- ) : recommendations.length > 0 ? ( -
- {recommendations.map((rec, index) => ( - -
-
-
- - {rec.priority} priority - -
-

{rec.title}

-

{rec.description}

-
-

- Recommended Action: {rec.action} -

-
-
-
-
- ))} -
- ) : ( -
- -

No recommendations needed - your security looks good!

-
- )} -
-
- ) -} diff --git a/frontend/archive/page-components/json/json-diff-stats.tsx b/frontend/archive/page-components/json/json-diff-stats.tsx deleted file mode 100644 index 8f3d448..0000000 --- a/frontend/archive/page-components/json/json-diff-stats.tsx +++ /dev/null @@ -1,552 +0,0 @@ -"use client" - -import React, { useState } from "react" -import { motion } from "framer-motion" -import { PlusCircle, MinusCircle, RefreshCw, Info, AlertTriangle, Shield, TrendingUp } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { compareJsonObjects, countChanges, type DiffResult, type DiffEntry } from "@/lib/json-diff" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Badge } from "@/components/ui/badge" -import type { JsonObject, JsonValue } from "@/lib/types" - -interface JsonDiffStatsProps { - baseData: JsonObject - compareData: JsonObject -} - -interface SecurityChange { - path: string - type: string - oldValue?: JsonValue - newValue?: JsonValue -} - -interface SecurityChanges { - expiration: SecurityChange[] - principals: SecurityChange[] - roles: SecurityChange[] - rules: SecurityChange[] - thresholds: SecurityChange[] - trust: SecurityChange[] - other: SecurityChange[] -} - -export default function JsonDiffStats({ baseData, compareData }: JsonDiffStatsProps) { - const [activeTab, setActiveTab] = useState<"summary" | "details" | "security">("summary") - - // Compare the two JSON objects - const diff = compareJsonObjects(baseData, compareData) - const { added, removed, changed, unchanged } = countChanges(diff) - - const total = added + removed + changed + unchanged - const addedPercent = Math.round((added / total) * 100) || 0 - const removedPercent = Math.round((removed / total) * 100) || 0 - const changedPercent = Math.round((changed / total) * 100) || 0 - const unchangedPercent = Math.round((unchanged / total) * 100) || 0 - - // Enhanced security analysis - const getSecurityAnalysis = (): SecurityChanges => { - const securityChanges: SecurityChanges = { - expiration: [], - principals: [], - roles: [], - rules: [], - thresholds: [], - trust: [], - other: [], - } - - const analyzeChange = (path: string, changeType: string, oldValue?: JsonValue, newValue?: JsonValue) => { - const pathLower = path.toLowerCase() - const change: SecurityChange = { path, type: changeType, oldValue, newValue } - - if (pathLower.includes("expire")) { - securityChanges.expiration.push(change) - } else if (pathLower.includes("principal")) { - securityChanges.principals.push(change) - } else if (pathLower.includes("role")) { - securityChanges.roles.push(change) - } else if (pathLower.includes("rule")) { - securityChanges.rules.push(change) - } else if (pathLower.includes("threshold")) { - securityChanges.thresholds.push(change) - } else if (pathLower.includes("trusted")) { - securityChanges.trust.push(change) - } else { - securityChanges.other.push(change) - } - } - - const traverse = (obj: Record | undefined, path = "") => { - if (!obj) return - - Object.entries(obj).forEach(([key, value]) => { - const currentPath = path ? `${path}.${key}` : key - - if (value.status === "added") { - analyzeChange(currentPath, "added", undefined, value.value) - } else if (value.status === "removed") { - analyzeChange(currentPath, "removed", value.value, undefined) - } else if (value.status === "changed") { - analyzeChange(currentPath, "changed", value.oldValue, value.value) - } - - if (value.children) { - traverse(value.children, currentPath) - } - }) - } - - if (diff && !('status' in diff)) { - traverse(diff as DiffResult) - } else if (diff && 'status' in diff && (diff as DiffEntry).children) { - traverse((diff as DiffEntry).children) - } - - return securityChanges - } - - const securityAnalysis = getSecurityAnalysis() - - // Find the most significant changes with enhanced categorization - const findSignificantChanges = () => { - const changes: { - path: string - type: string - oldValue?: JsonValue - newValue?: JsonValue - category: string - impact: "high" | "medium" | "low" - }[] = [] - - const getCategory = (path: string) => { - const pathLower = path.toLowerCase() - if (pathLower.includes("expire")) return "Security Expiration" - if (pathLower.includes("principal")) return "Security Principals" - if (pathLower.includes("role")) return "Access Roles" - if (pathLower.includes("rule")) return "Security Rules" - if (pathLower.includes("threshold")) return "Security Thresholds" - if (pathLower.includes("trusted")) return "Trust Settings" - if (pathLower.includes("github")) return "GitHub Integration" - return "Other" - } - - const getImpact = (path: string) => { - const pathLower = path.toLowerCase() - if (pathLower.includes("expire") || pathLower.includes("threshold") || pathLower.includes("trusted")) { - return "high" as const - } - if (pathLower.includes("principal") || pathLower.includes("role") || pathLower.includes("rule")) { - return "medium" as const - } - return "low" as const - } - - const traverse = (obj: Record | undefined, path = "") => { - if (!obj) return - - Object.entries(obj).forEach(([key, value]) => { - const currentPath = path ? `${path}.${key}` : key - - if (value.status === "added") { - changes.push({ - path: currentPath, - type: "added", - newValue: typeof value.value === "object" ? "Complex object" : value.value, - category: getCategory(currentPath), - impact: getImpact(currentPath), - }) - } else if (value.status === "removed") { - changes.push({ - path: currentPath, - type: "removed", - oldValue: typeof value.value === "object" ? "Complex object" : value.value, - category: getCategory(currentPath), - impact: getImpact(currentPath), - }) - } else if (value.status === "changed") { - changes.push({ - path: currentPath, - type: "changed", - oldValue: value.oldValue, - newValue: value.value, - category: getCategory(currentPath), - impact: getImpact(currentPath), - }) - } - - if (value.children) { - traverse(value.children, currentPath) - } - }) - } - - if (diff && !('status' in diff)) { - traverse(diff as DiffResult) - } else if (diff && 'status' in diff && (diff as DiffEntry).children) { - traverse((diff as DiffEntry).children) - } - - // Sort by impact level, then by category - return changes - .sort((a, b) => { - const impactOrder = { high: 0, medium: 1, low: 2 } - if (impactOrder[a.impact] !== impactOrder[b.impact]) { - return impactOrder[a.impact] - impactOrder[b.impact] - } - return a.category.localeCompare(b.category) - }) - .slice(0, 15) // Show top 15 changes - } - - const significantChanges = findSignificantChanges() - - return ( -
-
-
- - - -
-
- - {activeTab === "summary" && ( -
- - - - - - - - Added - - - -
{added}
-
elements ({addedPercent}%)
-
- -
-
-
-
- -
-

Added Elements

-

{added} new elements were added to the security metadata.

- {added > 0 && ( -
- Impact: New elements may introduce new security policies or - principals. -
- )} -
-
-
-
- - - - - - - - - Removed - - - -
{removed}
-
elements ({removedPercent}%)
-
- -
-
-
-
- -
-

Removed Elements

-

{removed} elements were removed from the security metadata.

- {removed > 0 && ( -
- Impact: Removed elements may affect security policies or - remove trusted principals. -
- )} -
-
-
-
- - - - - - - - - Changed - - - -
{changed}
-
elements ({changedPercent}%)
-
- -
-
-
-
- -
-

Changed Elements

-

{changed} elements were modified in the security metadata.

- {changed > 0 && ( -
- Impact: Modified elements may change security behavior. Pay - attention to threshold and expiry changes. -
- )} -
-
-
-
- - - - - - - Unchanged - - -
{unchanged}
-
elements ({unchangedPercent}%)
-
- -
-
-
-
- -
-

Unchanged Elements

-

{unchanged} elements remained the same between versions.

-
-
-
-
-
- )} - - {activeTab === "security" && ( -
-

- - Security Impact Analysis -

- -
- {Object.entries(securityAnalysis).map(([category, changes]) => { - if (changes.length === 0) return null - - const categoryNames: Record = { - expiration: "Security Expiration", - principals: "Security Principals", - roles: "Access Roles", - rules: "Security Rules", - thresholds: "Security Thresholds", - trust: "Trust Settings", - other: "Other Changes", - } - - const categoryIcons: Record = { - expiration: , - principals: , - roles: , - rules: , - thresholds: , - trust: , - other: , - } - - return ( - - - - {categoryIcons[category] || } - {categoryNames[category] || category} - - {changes.length} - - - - -
- {changes.slice(0, 3).map((change: SecurityChange, index: number) => ( -
-
{change.path}
-
- {change.type.charAt(0).toUpperCase() + change.type.slice(1)} -
-
- ))} - {changes.length > 3 && ( -
+{changes.length - 3} more changes
- )} -
-
-
- ) - })} -
-
- )} - - {activeTab === "details" && ( -
-

Detailed Change Analysis

- - {significantChanges.length > 0 ? ( -
- {significantChanges.map((change, index) => ( - -
-
-
- {change.type === "added" ? ( - - ) : change.type === "removed" ? ( - - ) : ( - - )} - {change.path} - - {change.category} - - - {change.impact} impact - -
- -
- {change.type === "changed" && ( -
-
-

Before:

- {String(change.oldValue)} -
-
-

After:

- {String(change.newValue)} -
-
- )} - {change.type === "added" && ( -
-

Added:

- {String(change.newValue)} -
- )} - {change.type === "removed" && ( -
-

Removed:

- {String(change.oldValue)} -
- )} -
-
-
-
- ))} -
- ) : ( -
-

No significant changes found

-
- )} -
- )} -
- ) -} diff --git a/frontend/archive/page-components/json/json-diff-visualization.tsx b/frontend/archive/page-components/json/json-diff-visualization.tsx deleted file mode 100644 index dca27fd..0000000 --- a/frontend/archive/page-components/json/json-diff-visualization.tsx +++ /dev/null @@ -1,762 +0,0 @@ -"use client" - -import type React from "react" - -import { useState, useCallback, useEffect } from "react" -import ReactFlow, { - Background, - Controls, - MiniMap, - Handle, - Position, - useNodesState, - useEdgesState, - addEdge, - type Node, - type Connection, - type Edge, -} from "reactflow" -import "reactflow/dist/style.css" -import dagre from "dagre" -import { motion } from "framer-motion" -import { CollapsibleCard } from "@/legacy/components/common/collapsible-card" -import { compareJsonObjects, type DiffEntry, type DiffResult } from "@/lib/json-diff" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Badge } from "@/components/ui/badge" -import { formatJsonValue, getNodeTypeDescription } from "@/lib/json-utils" -import type { JsonValue, JsonObject, JsonArray } from "@/lib/types" -import type { ViewMode } from "@/lib/view-mode-utils" - -// Node dimensions for layout -const NODE_WIDTH = 220 -const NODE_HEIGHT = 80 - -// Animated node wrapper -const AnimatedNode = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ) -} - -interface DiffNodeData { - label?: string - value?: JsonValue - oldValue?: JsonValue - newValue?: JsonValue - path?: string - isExpanded?: boolean - onToggle?: () => void - metadata?: Record - diffDetails?: string -} - -// Node tooltip wrapper -const DiffNodeTooltip = ({ children, data, type }: { children: React.ReactNode; data: DiffNodeData; type: string }) => { - return ( - - - {children} - -
-
-
- - {data.label || (type === "diffRoot" ? "Root" : "Node")} - - - {getNodeTypeDescription(type)} - -
-
-
- {data.path && ( -
- Path: - {data.path} -
- )} - - {type === "diffChanged" ? ( - <> -
- Old Value: -
-
{formatJsonValue(data.oldValue)}
-
-
-
- New Value: -
-
{formatJsonValue(data.newValue)}
-
-
- - ) : ( -
- Value: -
-
{formatJsonValue(data.value)}
-
-
- )} - - {data.metadata && Object.keys(data.metadata).length > 0 && ( -
- Metadata: -
- {Object.entries(data.metadata).map(([key, value]) => ( -
- {key}: {String(value)} -
- ))} -
-
- )} - - {type === "diffChanged" && data.diffDetails && ( -
- Change Details: -
{data.diffDetails}
-
- )} -
-
-
-
-
- ) -} - -// Node types -function DiffRootNode({ data, isConnectable }: { data: DiffNodeData; isConnectable: boolean }) { - return ( - - - - - - - - ) -} - -function DiffAddedNode({ data, isConnectable }: { data: DiffNodeData; isConnectable: boolean }) { - return ( - - - - -
- {formatJsonValue(data.value)} -
- -
-
-
- ) -} - -function DiffRemovedNode({ data, isConnectable }: { data: DiffNodeData; isConnectable: boolean }) { - return ( - - -
- -
-
{data.label}
-
- {formatJsonValue(data.value)} -
-
- -
-
-
- ) -} - -function DiffChangedNode({ data, isConnectable }: { data: DiffNodeData; isConnectable: boolean }) { - return ( - - -
- -
-
{data.label}
-
-
-
Old
-
{formatJsonValue(data.oldValue)}
-
-
-
New
-
{formatJsonValue(data.newValue)}
-
-
-
- -
-
-
- ) -} - -function DiffUnchangedNode({ data, isConnectable }: { data: DiffNodeData; isConnectable: boolean }) { - return ( - - -
- - - -
- {typeof data.value === "object" && data.value !== null - ? `Object with ${Object.keys(data.value).length} properties` - : data.value === null - ? "null" - : data.value === undefined - ? "undefined" - : String(data.value ?? "undefined")} -
-
-
-
-
- ) -} - -const nodeTypes = { - diffRoot: DiffRootNode, - diffAdded: DiffAddedNode, - diffRemoved: DiffRemovedNode, - diffChanged: DiffChangedNode, - diffUnchanged: DiffUnchangedNode, -} - -// Layout configuration -const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = "TB") => { - const dagreGraph = new dagre.graphlib.Graph() - dagreGraph.setDefaultEdgeLabel(() => ({})) - - const isHorizontal = direction === "LR" - dagreGraph.setGraph({ rankdir: direction, ranksep: 100, nodesep: 50 }) - - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT }) - }) - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target) - }) - - dagre.layout(dagreGraph) - - const layoutedNodes = nodes.map((node) => { - const nodeWithPosition = node - const { x, y } = dagreGraph.node(node.id) - - nodeWithPosition.position = { - x: isHorizontal ? x - NODE_WIDTH / 2 : x - NODE_WIDTH / 2, - y: isHorizontal ? y - NODE_HEIGHT / 2 : y - NODE_HEIGHT / 2, - } - - return nodeWithPosition - }) - - return { nodes: layoutedNodes, edges } -} - -// Main component -export interface JsonDiffVisualizationProps { - baseData: JsonObject | null - compareData: JsonObject | null - className?: string - viewMode?: ViewMode -} - -export default function JsonDiffVisualization({ - baseData, - compareData, -}: JsonDiffVisualizationProps) { - const [expandedNodes, setExpandedNodes] = useState>({}) - const [nodes, setNodes, onNodesChange] = useNodesState([]) - const [edges, setEdges, onEdgesChange] = useEdgesState([]) - const [showUnchanged, setShowUnchanged] = useState(true) - const [error, setError] = useState(null) - - const onConnect = useCallback((params: Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges]) - - const toggleNodeExpansion = useCallback((nodeId: string) => { - setExpandedNodes((prev) => ({ - ...prev, - [nodeId]: !prev[nodeId], - })) - }, []) - - // Process JSON data into nodes and edges - useEffect(() => { - if (!baseData || !compareData) return - - try { - const newNodes: Node[] = [] - const newEdges: Edge[] = [] - let nodeId = 0 - - // Add root node - const rootId = `node-${nodeId++}` - newNodes.push({ - id: rootId, - type: "diffRoot", - position: { x: 0, y: 0 }, - data: { - value: compareData, - isExpanded: true, - onToggle: () => toggleNodeExpansion(rootId), - path: "$", - metadata: { - type: typeof compareData, - schemaVersion: compareData?.schemaVersion || "N/A", - }, - }, - }) - - // Compare the two JSON objects - const diff = compareJsonObjects(baseData, compareData) - - if (!diff) { - setNodes(getLayoutedElements(newNodes, newEdges, "TB").nodes) - setEdges(newEdges) - return - } - - // Helper function to process added objects recursively - const processAddedObject = (parentId: string, obj: JsonObject | JsonArray, path: string, level: number) => { - Object.entries(obj).forEach(([childKey, childValue]) => { - const childId = `node-${nodeId++}` - const childPath = `${path}.${childKey}` - - newNodes.push({ - id: childId, - type: "diffAdded", - position: { x: 0, y: level * 100 }, - data: { - label: childKey, - value: childValue as JsonValue, - isExpanded: false, - path: childPath, - metadata: { - type: - typeof childValue === "object" - ? Array.isArray(childValue) - ? "array" - : "object" - : typeof childValue, - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${childId}`, - source: parentId, - target: childId, - animated: false, - style: { stroke: "#22c55e" }, - }) - - if (typeof childValue === "object" && childValue !== null) { - processAddedObject(childId, childValue, childPath, level + 1) - } - }) - } - - // Helper function to process removed objects recursively - const processRemovedObject = (parentId: string, obj: JsonObject | JsonArray, path: string, level: number) => { - Object.entries(obj).forEach(([childKey, childValue]) => { - const childId = `node-${nodeId++}` - const childPath = `${path}.${childKey}` - - newNodes.push({ - id: childId, - type: "diffRemoved", - position: { x: 0, y: level * 100 }, - data: { - label: childKey, - value: childValue as JsonValue, - isExpanded: false, - path: childPath, - metadata: { - type: - typeof childValue === "object" - ? Array.isArray(childValue) - ? "array" - : "object" - : typeof childValue, - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${childId}`, - source: parentId, - target: childId, - animated: false, - style: { stroke: "#ef4444" }, - }) - - if (typeof childValue === "object" && childValue !== null) { - processRemovedObject(childId, childValue, childPath, level + 1) - } - }) - } - - // Process diff recursively - const processDiff = (parentId: string, diffObj: DiffResult, path = "$", level = 1) => { - Object.entries(diffObj).forEach(([key, value]) => { - const currentId = `node-${nodeId++}` - const currentPath = path === "$" ? `${path}.${key}` : `${path}.${key}` - const isExpanded = expandedNodes[currentId] !== false - - if (value.status === "added") { - newNodes.push({ - id: currentId, - type: "diffAdded", - position: { x: 0, y: level * 100 }, - data: { - label: key, - value: value.value, - isExpanded, - onToggle: () => toggleNodeExpansion(currentId), - path: currentPath, - metadata: { - type: - typeof value.value === "object" - ? Array.isArray(value.value) - ? "array" - : "object" - : typeof value.value, - addedAt: new Date().toISOString(), - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: true, - style: { stroke: "#22c55e" }, - }) - - if (isExpanded && typeof value.value === "object" && value.value !== null) { - // For added objects, create nodes for their properties - const addedObj = value.value as JsonObject | JsonArray - processAddedObject(currentId, addedObj, currentPath, level + 1) - } - } else if (value.status === "removed") { - newNodes.push({ - id: currentId, - type: "diffRemoved", - position: { x: 0, y: level * 100 }, - data: { - label: key, - value: value.value, - isExpanded, - onToggle: () => toggleNodeExpansion(currentId), - path: currentPath, - metadata: { - type: - typeof value.value === "object" - ? Array.isArray(value.value) - ? "array" - : "object" - : typeof value.value, - removedAt: new Date().toISOString(), - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: true, - style: { stroke: "#ef4444" }, - }) - - if (isExpanded && typeof value.value === "object" && value.value !== null) { - // For removed objects, create nodes for their properties - const removedObj = value.value as JsonObject | JsonArray - processRemovedObject(currentId, removedObj, currentPath, level + 1) - } - } else if (value.status === "changed") { - // Generate a human-readable description of the change - let diffDetails = "Value changed" - if (typeof value.oldValue === "string" && typeof value.value === "string") { - if (value.oldValue.length !== value.value.length) { - diffDetails = `Length changed from ${value.oldValue.length} to ${value.value.length} characters` - } - } else if (typeof value.oldValue !== typeof value.value) { - diffDetails = `Type changed from ${typeof value.oldValue} to ${typeof value.value}` - } - - newNodes.push({ - id: currentId, - type: "diffChanged", - position: { x: 0, y: level * 100 }, - data: { - label: key, - oldValue: value.oldValue, - newValue: value.value, - isExpanded, - onToggle: () => toggleNodeExpansion(currentId), - path: currentPath, - metadata: { - oldType: typeof value.oldValue, - newType: typeof value.value, - changedAt: new Date().toISOString(), - }, - diffDetails, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: true, - style: { stroke: "#f59e0b" }, - }) - } else if (value.status === "unchanged" && showUnchanged) { - newNodes.push({ - id: currentId, - type: "diffUnchanged", - position: { x: 0, y: level * 100 }, - data: { - label: key, - value: value.value, - isExpanded, - onToggle: () => toggleNodeExpansion(currentId), - path: currentPath, - metadata: { - type: - typeof value.value === "object" - ? Array.isArray(value.value) - ? "array" - : "object" - : typeof value.value, - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: false, - style: { stroke: "#94a3b8" }, - }) - - if (isExpanded && typeof value.value === "object" && value.value !== null && value.children) { - processDiff(currentId, value.children, currentPath, level + 1) - } - } else if (value.children) { - // This is a nested object with changes inside, but the node itself is "unchanged" - // (checked above). If we are here, it means showUnchanged is false (otherwise caught above), - // OR the status was not added/removed/changed. - // Since we previously checked added/removed/changed, status is strictly "unchanged". - - // We usually want to show nodes that contain changes even if "showUnchanged" is false? - // If showUnchanged is false, the previous block skipped it. - // So this block executes for unchanged nodes with children. - // But if we want to hide unchanged nodes, we shouldn't render this node? - // However, if children have changes, we probably MUST render this node to maintain the tree. - - const nodeType = "diffUnchanged" - - newNodes.push({ - id: currentId, - type: nodeType, - position: { x: 0, y: level * 100 }, - data: { - label: key, - value: value.value || {}, - isExpanded, - onToggle: () => toggleNodeExpansion(currentId), - path: currentPath, - metadata: { - type: - typeof value.value === "object" - ? Array.isArray(value.value) - ? "array" - : "object" - : typeof value.value, - hasNestedChanges: true, - }, - }, - }) - - const edgeColor = "#94a3b8" - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: false, - style: { stroke: edgeColor }, - }) - - if (isExpanded) { - processDiff(currentId, value.children, currentPath, level + 1) - } - } - }) - } - - // Check if diff is a single entry (e.g., entire object added/removed) or a Result Record - if ('status' in diff && typeof (diff as DiffEntry).status === 'string') { - const diffEntry = diff as DiffEntry; - // If the whole thing is added, we iterate its value if it's an object - if (diffEntry.status === "added" && typeof diffEntry.value === "object" && diffEntry.value !== null) { - processAddedObject(rootId, diffEntry.value as JsonObject | JsonArray, "$", 1); - } else if (diffEntry.status === "removed" && typeof diffEntry.value === "object" && diffEntry.value !== null) { - processRemovedObject(rootId, diffEntry.value as JsonObject | JsonArray, "$", 1); - } - // Handle other cases if necessary (e.g. root changed type) - } else { - // Start processing from root as a DiffResult - processDiff(rootId, diff as DiffResult, "$") - } - - // Apply layout - const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( - newNodes, - newEdges, - "TB", // Top to Bottom layout - ) - - setNodes(layoutedNodes) - setEdges(layoutedEdges) - setError(null) - } catch (err) { - console.error("Error processing diff data:", err) - setError("Failed to process comparison data. Please try again.") - } - }, [baseData, compareData, expandedNodes, toggleNodeExpansion, showUnchanged, setNodes, setEdges]) - - return ( - - - { - if (n.type === "diffAdded") return "#22c55e" - if (n.type === "diffRemoved") return "#ef4444" - if (n.type === "diffChanged") return "#f59e0b" - return "#94a3b8" - }} - nodeColor={(n) => { - if (n.type === "diffAdded") return "#dcfce7" - if (n.type === "diffRemoved") return "#fee2e2" - if (n.type === "diffChanged") return "#fef3c7" - return "#f1f5f9" - }} - /> - -
- -
- {error && ( -
- {error} -
- )} -
- ) -} diff --git a/frontend/archive/page-components/json/json-tree-view.tsx b/frontend/archive/page-components/json/json-tree-view.tsx deleted file mode 100644 index 68391eb..0000000 --- a/frontend/archive/page-components/json/json-tree-view.tsx +++ /dev/null @@ -1,377 +0,0 @@ -"use client" - -import React, { useState } from "react" -import { ChevronDown, ChevronRight, Calendar, Key, Users, Shield, Clock, FileText, Hash, Globe } from "lucide-react" -import { Badge } from "@/components/ui/badge" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { shouldShowInNormalMode, type ViewMode } from "@/lib/view-mode-utils" -import type { JsonValue, JsonArray, JsonObject } from "@/lib/types" - -interface JsonTreeViewProps { - jsonData: JsonValue - viewMode?: ViewMode -} - -interface TreeNodeProps { - data: JsonValue - keyName?: string - level?: number - isLast?: boolean - parentPath?: string - viewMode?: ViewMode -} - -const getValueColor = (value: JsonValue): string => { - if (typeof value === "string") return "text-green-600" - if (typeof value === "number") return "text-orange-600" - if (typeof value === "boolean") return "text-blue-600" - if (value === null) return "text-gray-500" - return "text-gray-800" -} - -const getValueIcon = (key: string) => { - const keyLower = key.toLowerCase() - - if (keyLower.includes("expire")) return - if (keyLower.includes("key") || keyLower === "principals") return - if (keyLower.includes("role")) return - if (keyLower.includes("security") || keyLower.includes("trusted")) - return - if (keyLower.includes("version")) return - if (keyLower.includes("url") || keyLower.includes("issuer")) return - if (keyLower.includes("time") || keyLower.includes("date")) return - - return -} - -const getSecurityBadge = () => { - // Removed all security badges to clean up the UI - return null -} - -const formatValue = (value: JsonValue): string => { - if (value === null) return "null" - if (value === undefined) return "undefined" - if (typeof value === "string") return `"${value}"` - return String(value) -} - -const getTooltipContent = (key: string, value: JsonValue, path: string) => { - const keyLower = key.toLowerCase() - - // Root-level metadata fields - if (keyLower === "type") { - return ( -
-
- Metadata Type -
-
Value: {value as string}
-
- Specifies the type of gittuf metadata. "root" contains trust anchors and role definitions, while - "targets" contains security policies and rules. -
-
Path: {path}
-
- ) - } - - if (keyLower === "schemaversion") { - return ( -
-
- Schema Version -
-
Version: {value as string | number}
-
Defines the structure and validation rules for this metadata.
-
Path: {path}
-
- ) - } - - if (keyLower.includes("expire")) { - const expiryDate = new Date(value as string) - const now = new Date() - const daysUntilExpiry = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) - const isExpired = expiryDate < now - - return ( -
-
- Expiration Date -
-
Date: {expiryDate.toLocaleDateString()}
-
Time: {expiryDate.toLocaleTimeString()}
-
- Status: {isExpired ? "EXPIRED" : `${daysUntilExpiry} days remaining`} -
-
Security metadata must be refreshed before this date.
-
Path: {path}
-
- ) - } - - // Principal-related fields - if (keyLower === "principals") { - const count = typeof value === "object" && value !== null ? Object.keys(value).length : 0 - return ( -
-
- Security Principals -
-
Count: {count} principals
-
- Contains keys and identities that can sign and validate repository operations. -
-
Path: {path}
-
- ) - } - - if (keyLower === "keytype") { - return ( -
-
- Key Type -
-
Type: {value as string}
-
Specifies the key algorithm used for signing.
-
Path: {path}
-
- ) - } - - if (keyLower === "threshold") { - return ( -
-
- Signature Threshold -
-
Required signatures: {value as number}
-
- Minimum number of valid signatures required from the authorized principals. -
-
Path: {path}
-
- ) - } - - // Generic fallback with simplified context - const getGenericDescription = (key: string, value: JsonValue) => { - if (typeof value === "object" && value !== null) { - const isArray = Array.isArray(value) - const count = isArray ? value.length : Object.keys(value).length - return `${isArray ? "Array" : "Object"} containing ${count} ${isArray ? "items" : "properties"}.` - } - - if (typeof value === "string" && value.startsWith("https://")) { - return "URL reference to an external resource." - } - - if (typeof value === "number") { - return "Numeric value." - } - - if (typeof value === "boolean") { - return "Boolean flag controlling a security feature." - } - - return "Security metadata field used by gittuf." - } - - return ( -
-
- {key} -
-
Value: {formatValue(value)}
-
Type: {typeof value}
-
{getGenericDescription(key, value)}
-
Path: {path}
-
- ) -} - -const TreeNode: React.FC = ({ - data, - keyName, - level = 0, - parentPath = "", - viewMode = "advanced", -}) => { - const [isExpanded, setIsExpanded] = useState(level < 2) // Auto-expand first 2 levels - - const currentPath = parentPath ? `${parentPath}.${keyName}` : keyName || "root" - const isObject = typeof data === "object" && data !== null && !Array.isArray(data) - const isArray = Array.isArray(data) - const isPrimitive = !isObject && !isArray - - // Check if this node should be shown in normal mode - const shouldShow = viewMode === "advanced" || !keyName || shouldShowInNormalMode(keyName, data, level) - - if (!shouldShow) { - return null - } - - const toggleExpanded = () => { - if (!isPrimitive) { - setIsExpanded(!isExpanded) - } - } - - const renderConnector = () => { - if (level === 0) return null - - return ( -
- {Array.from({ length: level }).map((_, i) => ( -
- {i === level - 1 ?
:
} -
- ))} -
- ) - } - - const renderValue = () => { - if (isPrimitive) { - const securityBadge = keyName ? getSecurityBadge() : null - const icon = keyName ? getValueIcon(keyName) : null - - return ( - - - -
- {icon} - {keyName}: - {formatValue(data)} - {securityBadge} -
-
- - {keyName && getTooltipContent(keyName, data, currentPath)} - -
-
- ) - } - - const objectKeys = isObject && data ? Object.keys(data) : [] - const arrayLength = isArray ? (data as JsonArray).length : 0 - const count = isObject ? objectKeys.length : arrayLength - const securityBadge = keyName ? getSecurityBadge() : null - const icon = keyName ? getValueIcon(keyName) : - - // In normal mode, count only visible children - let visibleCount = count - if (viewMode === "normal" && isObject && data) { - visibleCount = objectKeys.filter((key) => shouldShowInNormalMode(key, (data as JsonObject)[key], level + 1)).length - } - - return ( -
- - - -
-
- {isExpanded ? ( - - ) : ( - - )} -
- {icon} - - {keyName || "root"} {isObject ? "{" : "["} - - - {viewMode === "normal" ? visibleCount : count} {isObject ? "keys" : "items"} - {viewMode === "normal" && visibleCount !== count && ( - ({count} total) - )} - - {securityBadge} -
-
- -
-
- {keyName || "Root Object"} -
-
Type: {isArray ? "Array" : "Object"}
-
- Size: {count} {isObject ? "properties" : "items"} - {viewMode === "normal" && visibleCount !== count && ( -
Showing {visibleCount} important items in normal mode
- )} -
-
Path: {currentPath}
-
-
-
-
- - {isExpanded && ( -
- {isObject && - objectKeys.map((key, index) => ( - - ))} - {isArray && - (data as JsonArray).map((item, index) => ( - - ))} -
- )} -
- ) - } - - return ( -
-
- {renderConnector()} - {renderValue()} -
-
- ) -} - -export default function JsonTreeView({ jsonData, viewMode = "normal" }: JsonTreeViewProps) { - if (!jsonData) { - return ( -
-

No JSON data to display

-
- ) - } - - return ( -
-
- -
-
- ) -} diff --git a/frontend/archive/page-components/json/json-tree-visualization.tsx b/frontend/archive/page-components/json/json-tree-visualization.tsx deleted file mode 100644 index b440b92..0000000 --- a/frontend/archive/page-components/json/json-tree-visualization.tsx +++ /dev/null @@ -1,475 +0,0 @@ -"use client" - -import type React from "react" - -import { useState, useCallback, useEffect } from "react" -import ReactFlow, { - Background, - Controls, - MiniMap, - Handle, - Position, - useNodesState, - useEdgesState, - addEdge, - type Node, - type Edge, - type Connection, - type NodeProps, -} from "reactflow" -import "reactflow/dist/style.css" -import dagre from "dagre" -import { CollapsibleCard } from "@/legacy/components/common/collapsible-card" -import { motion } from "framer-motion" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Badge } from "@/components/ui/badge" -import { formatJsonValue, getNodeTypeDescription } from "@/lib/json-utils" -import { shouldShowInNormalMode, type ViewMode } from "@/lib/view-mode-utils" -import type { JsonValue, JsonArray, JsonObject } from "@/lib/types" - -// Node dimensions for layout -const NODE_WIDTH = 220 -const NODE_HEIGHT = 80 - -interface CustomNodeData { - label?: string - value: JsonValue - isExpanded?: boolean - onToggle?: () => void - path?: string - metadata?: Record -} - -// Animated node wrapper -const AnimatedNode = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ) -} - -// Node tooltip wrapper -const NodeTooltip = ({ children, data, type }: { children: React.ReactNode; data: CustomNodeData; type: string }) => { - return ( - - - {children} - -
-
-
- - {data.label || (type === "rootNode" ? "Root" : "Node")} - - - {getNodeTypeDescription(type)} - -
-
-
- {data.path && ( -
- Path: - {data.path} -
- )} -
- Value: -
-
{formatJsonValue(data.value)}
-
-
- {data.metadata && Object.keys(data.metadata).length > 0 && ( -
- Metadata: -
- {Object.entries(data.metadata).map(([key, value]) => ( -
- {key}: {String(value)} -
- ))} -
-
- )} -
-
-
-
-
- ) -} - -// Node types -function RootNode({ data, isConnectable }: NodeProps) { - return ( - - -
- - -
- {typeof data.value === "object" && data.value !== null - ? `Object with ${Object.keys(data.value).length} properties` - : data.value === null - ? "null" - : data.value === undefined - ? "undefined" - : String(data.value)} -
-
-
-
-
- ) -} - -function JsonNode({ data, isConnectable }: NodeProps) { - return ( - - -
- - - -
- {typeof data.value === "object" && data.value !== null - ? `Object with ${Object.keys(data.value).length} properties` - : data.value === null - ? "null" - : data.value === undefined - ? "undefined" - : String(data.value)} -
-
-
-
-
- ) -} - -function ArrayNode({ data, isConnectable }: NodeProps) { - return ( - - -
- - - -
Array with {(data.value as JsonArray).length} items
-
-
-
-
- ) -} - -function ValueNode({ data, isConnectable }: NodeProps) { - return ( - - -
- - -
- {data.value === null ? "null" : data.value === undefined ? "undefined" : String(data.value)} -
-
-
-
-
- ) -} - -const nodeTypes = { - jsonNode: JsonNode, - arrayNode: ArrayNode, - valueNode: ValueNode, - rootNode: RootNode, -} - -// Layout configuration -const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = "TB") => { - const dagreGraph = new dagre.graphlib.Graph() - dagreGraph.setDefaultEdgeLabel(() => ({})) - - const isHorizontal = direction === "LR" - dagreGraph.setGraph({ rankdir: direction, ranksep: 100, nodesep: 50 }) - - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT }) - }) - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target) - }) - - dagre.layout(dagreGraph) - - const layoutedNodes = nodes.map((node) => { - const nodeWithPosition = node - const { x, y } = dagreGraph.node(node.id) - - nodeWithPosition.position = { - x: isHorizontal ? x - NODE_WIDTH / 2 : x - NODE_WIDTH / 2, - y: isHorizontal ? y - NODE_HEIGHT / 2 : y - NODE_HEIGHT / 2, - } - - return nodeWithPosition - }) - - return { nodes: layoutedNodes, edges } -} - -// Main component -export default function JsonTreeVisualization({ - jsonData, - viewMode = "advanced", -}: { jsonData: JsonValue; viewMode?: ViewMode }) { - const [expandedNodes, setExpandedNodes] = useState>({}) - const [nodes, setNodes, onNodesChange] = useNodesState([]) - const [edges, setEdges, onEdgesChange] = useEdgesState([]) - - const onConnect = useCallback((params: Connection) => setEdges((eds) => addEdge(params, eds)), [setEdges]) - - const toggleNodeExpansion = useCallback((nodeId: string) => { - setExpandedNodes((prev) => ({ - ...prev, - [nodeId]: !prev[nodeId], - })) - }, []) - - // Process JSON data into nodes and edges - useEffect(() => { - if (!jsonData) return - - const newNodes: Node[] = [] - const newEdges: Edge[] = [] - let nodeId = 0 - - // Add root node - const rootId = `node-${nodeId++}` - newNodes.push({ - id: rootId, - type: "rootNode", - position: { x: 0, y: 0 }, - data: { - value: jsonData, - isExpanded: true, - onToggle: () => toggleNodeExpansion(rootId), - path: "$", - metadata: { - type: typeof jsonData, - schemaVersion: (jsonData as JsonObject)?.schemaVersion || "N/A", - }, - }, - }) - - // Process JSON recursively - const processJson = (parentId: string, data: JsonValue, path = "", level = 1) => { - if (data === null || data === undefined) { - return - } - - if (Array.isArray(data)) { - // Process array - data.forEach((item, index) => { - const currentId = `node-${nodeId++}` - const currentPath = path ? `${path}[${index}]` : `[${index}]` - const isExpanded = expandedNodes[currentId] !== false - - // Check if this node should be shown in normal mode - const shouldShow = viewMode === "advanced" || shouldShowInNormalMode(`[${index}]`, item, level) - if (!shouldShow) return - - if (typeof item === "object" && item !== null) { - // Object or array inside array - newNodes.push({ - id: currentId, - type: Array.isArray(item) ? "arrayNode" : "jsonNode", - position: { x: 0, y: level * 100 }, - data: { - label: `[${index}]`, - value: item, - isExpanded, - onToggle: () => toggleNodeExpansion(currentId), - path: currentPath, - metadata: { - type: Array.isArray(item) ? "array" : "object", - size: Array.isArray(item) ? item.length : Object.keys(item).length, - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: true, - style: { stroke: Array.isArray(item) ? "#22c55e" : "#a855f7" }, - }) - - if (isExpanded) { - processJson(currentId, item, currentPath, level + 1) - } - } else { - // Primitive value inside array - newNodes.push({ - id: currentId, - type: "valueNode", - position: { x: 0, y: level * 100 }, - data: { - label: `[${index}]`, - value: item, - path: currentPath, - metadata: { - type: typeof item, - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: true, - style: { stroke: "#f59e0b" }, - }) - } - }) - } else if (typeof data === "object" && data !== null) { - // Process object - Object.entries(data).forEach(([key, value]) => { - const currentId = `node-${nodeId++}` - const currentPath = path ? `${path}.${key}` : key - const isExpanded = expandedNodes[currentId] !== false - - // Check if this node should be shown in normal mode - const shouldShow = viewMode === "advanced" || shouldShowInNormalMode(key, value, level) - if (!shouldShow) return - - if (typeof value === "object" && value !== null) { - // Nested object or array - newNodes.push({ - id: currentId, - type: Array.isArray(value) ? "arrayNode" : "jsonNode", - position: { x: 0, y: level * 100 }, - data: { - label: key, - value, - isExpanded, - onToggle: () => toggleNodeExpansion(currentId), - path: currentPath, - metadata: { - type: Array.isArray(value) ? "array" : "object", - size: Array.isArray(value) ? value.length : Object.keys(value).length, - ...(key === "schemaVersion" && { important: true }), - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: true, - style: { stroke: Array.isArray(value) ? "#22c55e" : "#a855f7" }, - }) - - if (isExpanded) { - processJson(currentId, value, currentPath, level + 1) - } - } else { - // Primitive value - newNodes.push({ - id: currentId, - type: "valueNode", - position: { x: 0, y: level * 100 }, - data: { - label: key, - value, - path: currentPath, - metadata: { - type: typeof value, - ...(key === "schemaVersion" && { important: true }), - ...(key === "expires" && { timeValue: true }), - }, - }, - }) - - newEdges.push({ - id: `edge-${parentId}-${currentId}`, - source: parentId, - target: currentId, - animated: false, - style: { stroke: "#f59e0b" }, - }) - } - }) - } - } - - // Start processing from root - processJson(rootId, jsonData, "$") - - // Apply layout - const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( - newNodes, - newEdges, - "TB", // Top to Bottom layout - ) - - setNodes(layoutedNodes) - setEdges(layoutedEdges) - }, [jsonData, expandedNodes, toggleNodeExpansion, viewMode, setNodes, setEdges]) - - return ( - - - { - if (n.type === "rootNode") return "#4f46e5" // indigo-600 - if (n.type === "jsonNode") return "#7c3aed" // purple-600 - if (n.type === "arrayNode") return "#0891b2" // cyan-600 - return "#d97706" // amber-600 - }} - nodeColor={(n) => { - if (n.type === "rootNode") return "#e0e7ff" // indigo-100 - if (n.type === "jsonNode") return "#f3e8ff" // purple-100 - if (n.type === "arrayNode") return "#cffafe" // cyan-100 - return "#fef3c7" // amber-100 - }} - /> - - - ) -} diff --git a/frontend/archive/page-components/json/tree-view-tab.tsx b/frontend/archive/page-components/json/tree-view-tab.tsx deleted file mode 100644 index e78ce1e..0000000 --- a/frontend/archive/page-components/json/tree-view-tab.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client" - -import { FileJson, Loader2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import JsonTreeView from "@/legacy/page-components/json/json-tree-view" -import type { Commit } from "@/lib/types" -import type { ViewMode } from "@/lib/view-mode-utils" -import type { JsonValue } from "@/lib/types" - -interface TreeViewTabProps { - selectedCommit: Commit | null - selectedFile: string - isLoading: boolean - error: string - jsonData: JsonValue - viewMode: ViewMode - onRetry: () => void -} - -export default function TreeViewTab({ - selectedCommit, - selectedFile, - isLoading, - error, - jsonData, - viewMode, - onRetry, -}: TreeViewTabProps) { - return ( - <> - {selectedCommit && ( -
- - -
-
-

- - Structured Tree View: {selectedFile} -

-
- - {selectedCommit.hash.substring(0, 8)} - -

{selectedCommit.message}

-
-
- - {new Date(selectedCommit.date).toLocaleDateString()} by {selectedCommit.author} - -
-
-
-
- )} - -
- {isLoading ? ( -
- - Loading tree structure... -
- ) : error ? ( -
- -

Error Loading Tree View

-

{error}

- -
- ) : jsonData && selectedCommit ? ( - - ) : ( -
- -

Tree View Ready!

-

- Select a commit to explore the security metadata in a familiar tree structure - perfect for beginners! -

-
- )} -
- - ) -} diff --git a/frontend/archive/page-components/repository/repository-status.tsx b/frontend/archive/page-components/repository/repository-status.tsx deleted file mode 100644 index c7e78f7..0000000 --- a/frontend/archive/page-components/repository/repository-status.tsx +++ /dev/null @@ -1,202 +0,0 @@ -"use client" - -import { useMemo } from "react" -import { motion } from "framer-motion" -import { - GitBranch, - Globe, - HardDrive, - Calendar, - User, - FileText, - CheckCircle, - AlertTriangle, - RefreshCw, -} from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import type { RepositoryInfo } from "@/lib/repository-handler" -import type { Commit } from "@/lib/types" - -interface RepositoryStatusProps { - repository: RepositoryInfo - commits: Commit[] - onRefresh: () => void - isLoading: boolean -} - -export default function RepositoryStatus({ repository, commits, onRefresh, isLoading }: RepositoryStatusProps) { - const repoStats = useMemo(() => { - if (commits.length === 0) return null - - const sortedCommits = [...commits].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) - const authors = [...new Set(commits.map((c) => c.author))] - - return { - totalCommits: commits.length, - dateRange: { - start: sortedCommits[0].date, - end: sortedCommits[sortedCommits.length - 1].date, - }, - authors, - hasSecurityFiles: true, // Assume true if we have commits - lastUpdate: sortedCommits[sortedCommits.length - 1].date, - } - }, [commits]) - - const getRepositoryIcon = () => { - if (repository.type === "remote") { - if (repository.path.includes("github.com")) return - if (repository.path.includes("gitlab.com")) return - return - } - return - } - - const getRepositoryPlatform = () => { - if (repository.type === "local") return "Local Repository" - if (repository.path.includes("github.com")) return "GitHub" - if (repository.path.includes("gitlab.com")) return "GitLab" - if (repository.path.includes("bitbucket.org")) return "Bitbucket" - return "Remote Repository" - } - - return ( - - -
-
-
{getRepositoryIcon()}
-
- - {repository.name} - - {getRepositoryPlatform()} - - -

- {repository.type === "remote" ? repository.path : `Local: ${repository.path}`} -

-
-
- - - - - - -

Refresh repository data

-
-
-
-
-
- - {repoStats && ( - -
- -
- - Commits -
-
{repoStats.totalCommits}
-
- - -
- - Authors -
-
{repoStats.authors.length}
-
- - -
- - Security Files -
-
- {repoStats.hasSecurityFiles ? ( - - ) : ( - - )} - - {repoStats.hasSecurityFiles ? "Found" : "Missing"} - -
-
- - -
- - Last Update -
-
- {new Date(repoStats.lastUpdate).toLocaleDateString()} -
-
-
- - {/* Authors List */} - {repoStats.authors.length > 0 && ( - -
- - Contributors -
-
- {repoStats.authors.slice(0, 5).map((author, index) => ( - - {author} - - ))} - {repoStats.authors.length > 5 && ( - - +{repoStats.authors.length - 5} more - - )} -
-
- )} -
- )} -
- ) -} diff --git a/frontend/archive/page-components/visualization/file-selector.tsx b/frontend/archive/page-components/visualization/file-selector.tsx deleted file mode 100644 index d174594..0000000 --- a/frontend/archive/page-components/visualization/file-selector.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client" - -import { FileJson } from "lucide-react" -import { FILENAMES } from "@/lib/constants" -import { Button } from "@/components/ui/button" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import EnhancedViewModeToggle from "@/legacy/components/common/enhanced-view-mode-toggle" -import type { ViewMode } from "@/lib/view-mode-utils" - -interface FileSelectorProps { - selectedFile: string - onFileChange: (file: string) => void - viewMode: ViewMode - onViewModeChange: (mode: ViewMode) => void - hiddenCount: number - showViewToggle: boolean -} - -export default function FileSelector({ - selectedFile, - onFileChange, - viewMode, - onViewModeChange, - hiddenCount, - showViewToggle, -}: FileSelectorProps) { - return ( -
-
-
-
- - Security Files: -
- - - - - - -
-

Root Security Policy

-

- Contains trust anchors, keys, and role definitions that form the foundation of repository security. -

-
-
-
-
- - - - - - - -
-

Target Security Rules

-

- Contains specific security policies and rules that control who can modify different parts of the - repository. -

-
-
-
-
-
-
- - {showViewToggle && ( - - )} -
- ) -} diff --git a/frontend/archive/page-components/visualization/visualization-tab.tsx b/frontend/archive/page-components/visualization/visualization-tab.tsx deleted file mode 100644 index 68da1b9..0000000 --- a/frontend/archive/page-components/visualization/visualization-tab.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client" - -import { FileJson, Loader2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import type { Commit } from "@/lib/types" -import type { ViewMode } from "@/lib/view-mode-utils" -import dynamic from "next/dynamic" - -const JsonTreeVisualization = dynamic(() => import("@/legacy/page-components/json/json-tree-visualization"), { - ssr: false, - loading: () => ( -
- - Loading visualization... -
- ), -}) -import type { JsonValue } from "@/lib/types" - -interface VisualizationTabProps { - selectedCommit: Commit | null - selectedFile: string - isLoading: boolean - error: string - jsonData: JsonValue - viewMode: ViewMode - onRetry: () => void -} - -export default function VisualizationTab({ - selectedCommit, - selectedFile, - isLoading, - error, - jsonData, - viewMode, - onRetry, -}: VisualizationTabProps) { - return ( - <> - {selectedCommit && ( -
- - -
-
-

- - Interactive Graph View: {selectedFile} -

-
- - {selectedCommit.hash.substring(0, 8)} - -

{selectedCommit.message}

-
-
- - {new Date(selectedCommit.date).toLocaleDateString()} by {selectedCommit.author} - -
-
-
-
- )} - -
- {isLoading ? ( -
- - Loading security metadata visualization... -
- ) : error ? ( -
- -

Error Loading Visualization

-

{error}

- -
- ) : jsonData && selectedCommit ? ( - - ) : ( -
- -

Ready to Visualize!

-

- Select a commit from the "Browse Commits" tab to see an interactive graph of the security metadata -

-
- )} -
- - ) -} diff --git a/frontend/archive/playground/app/page.tsx b/frontend/archive/playground/app/page.tsx deleted file mode 100644 index 9dfba76..0000000 --- a/frontend/archive/playground/app/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client" - -import { AnimatePresence, motion } from "framer-motion" -import { StoryModal } from "@/components/common/story-modal" -import { StatusCard } from "@/components/common/status-card" -import { useGittufSimulator } from "@/hooks/use-gittuf-simulator" -import { SimulatorHeader } from "@/screens/playground/simulator-header" -import { SimulatorControls } from "@/screens/playground/simulator-controls" -import { SimulatorGraph } from "@/screens/playground/simulator-graph" -import { SimulatorAnalysis } from "@/screens/playground/simulator-analysis" -import { SimulatorGlossary } from "@/screens/playground/simulator-glossary" -import { SimulatorConfigModal } from "@/screens/playground/simulator-config-modal" - -export default function SimulatorPage() { - const state = useGittufSimulator() - const { - darkMode, - showStory, - setShowStory, - showSimulator, - setShowSimulator, - displayResult, - expandedGraph, - fixture, - } = state - - return ( -
- {/* Story Modal */} - setShowStory(false)} - fixture={fixture} - onOpenSimulator={() => { - setShowStory(false) - setShowSimulator(true) - }} - /> - -
- {/* Header */} - - - {/* Main Simulator UI */} - - {showSimulator && ( - - {/* Status Card */} - - - {/* Main Content Area */} -
- - -
- - {/* Detailed Results */} - -
- )} -
- - {/* Footer Glossary */} - -
- - {/* Custom Config Dialog */} - -
- ) -} diff --git a/frontend/archive/playground/components/status-card.tsx b/frontend/archive/playground/components/status-card.tsx deleted file mode 100644 index 43a3ec7..0000000 --- a/frontend/archive/playground/components/status-card.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client" - -import { motion } from "framer-motion" -import { CheckCircle, XCircle } from "lucide-react" -import { Card, CardContent } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { useState } from "react" - -interface StatusCardProps { - result: "allowed" | "blocked" - reasons: string[] - className?: string -} - -export function StatusCard({ result, reasons, className = "" }: StatusCardProps) { - const [explainLikeIm5, setExplainLikeIm5] = useState(false) - - const getSimpleExplanation = () => { - if (result === "allowed") { - return "✅ Good to go! Everyone who needed to approve this change has signed off." - } else { - return "⛔ Hold on! This change needs more approvals before it can go through." - } - } - - const getDetailedExplanation = () => { - return reasons.join(" ") - } - - return ( - - - -
- - {result === "allowed" ? ( - - ) : ( - - )} - - -
-
- - {result} - - - {result === "allowed" ? "Safe to proceed" : "Needs attention"} - -
- - -
- -
- - - {explainLikeIm5 ? getSimpleExplanation() : getDetailedExplanation()} - -
- - {/* Pulse animation for blocked status */} - {result === "blocked" && ( - - )} -
-
-
-
-
- ) -} diff --git a/frontend/archive/playground/components/story-modal.tsx b/frontend/archive/playground/components/story-modal.tsx deleted file mode 100644 index 773edec..0000000 --- a/frontend/archive/playground/components/story-modal.tsx +++ /dev/null @@ -1,371 +0,0 @@ -"use client" - -import { useState, useEffect, useCallback } from "react" -import { motion, AnimatePresence } from "framer-motion" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Card, CardContent } from "@/components/ui/card" -import { - X, - ChevronLeft, - ChevronRight, - Play, - Pause, - Shield, - Users, - CheckCircle, - XCircle, - Crown, - Key, - FileText, -} from "lucide-react" -import { TrustGraph } from "@/screens/playground/trust-graph" -import type { SimulatorResponse } from "@/lib/simulator-types" - -interface StoryModalProps { - isOpen: boolean - onClose: () => void - fixture: SimulatorResponse - onOpenSimulator: () => void -} - -const storySteps = [ - { - id: "root", - title: "Root of Trust", - icon: Crown, - description: - "Every repository starts with a root role that holds the ultimate authority. Think of it as the master key that can delegate permissions to others.", - focusNodes: ["root"], - explanation: "The root role is like the owner of a house who can give keys to trusted people.", - }, - { - id: "delegation", - title: "Delegations", - icon: Key, - description: - "The root delegates specific permissions to roles. Each role protects certain files and requires a minimum number of approvals (threshold).", - focusNodes: ["root", "protect-main"], - explanation: "Roles are like security guards assigned to protect specific areas of your code.", - }, - { - id: "approvals", - title: "Approvals & Signatures", - icon: Users, - description: - "People with the right permissions can approve changes by signing them. Each signature is verified to ensure it's authentic.", - focusNodes: ["alice", "bob", "charlie"], - explanation: "Signatures are like digital stamps of approval from trusted team members.", - }, - { - id: "decision", - title: "Final Decision", - icon: Shield, - description: - "The system checks if enough valid signatures were collected to meet each role's threshold. If yes, the change is allowed!", - focusNodes: [], - explanation: "It's like checking if you have enough votes to pass a motion in a meeting.", - }, -] - -export function StoryModal({ isOpen, onClose, fixture, onOpenSimulator }: StoryModalProps) { - const [currentStep, setCurrentStep] = useState(0) - const [autoPlay, setAutoPlay] = useState(false) - const [animatePulse, setAnimatePulse] = useState(false) - - useEffect(() => { - if (!autoPlay) return - - const interval = setInterval(() => { - setCurrentStep((prev) => { - if (prev >= storySteps.length - 1) { - setAutoPlay(false) - return prev - } - return prev + 1 - }) - }, 4000) - - return () => clearInterval(interval) - }, [autoPlay]) - const currentStepData = storySteps[currentStep] - const StepIcon = currentStepData.icon - - const handleNext = useCallback(() => { - if (currentStep < storySteps.length - 1) { - setCurrentStep(currentStep + 1) - } - }, [currentStep]) - - const handlePrev = useCallback(() => { - if (currentStep > 0) { - setCurrentStep(currentStep - 1) - } - }, [currentStep]) - - const handleKeyDown = useCallback((e: KeyboardEvent) => { - if (!isOpen) return - - switch (e.key) { - case "ArrowRight": - e.preventDefault() - handleNext() - break - case "ArrowLeft": - e.preventDefault() - handlePrev() - break - case "Escape": - e.preventDefault() - onClose() - break - case " ": - e.preventDefault() - setAutoPlay(!autoPlay) - break - } - }, [isOpen, autoPlay, handleNext, handlePrev, onClose]) - - useEffect(() => { - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [handleKeyDown]) - - useEffect(() => { - if (isOpen) { - // Use setTimeout to avoid synchronous state update warning during render phase - const pulseTimer = setTimeout(() => { - setAnimatePulse(true) - }, 0) - - const resetTimer = setTimeout(() => setAnimatePulse(false), 3000) - - return () => { - clearTimeout(pulseTimer) - clearTimeout(resetTimer) - } - } - }, [currentStep, isOpen]) - - return ( - - {isOpen && ( - - e.stopPropagation()} - > - {/* Header */} -
-
-
- -
-
-

{currentStepData.title}

-

- Step {currentStep + 1} of {storySteps.length} -

-
-
- -
- - -
-
- - {/* Progress Bar */} -
- -
- - {/* Content */} -
- {/* Left: Explanation */} - - - -

What's happening here?

-

{currentStepData.description}

-
-

💡 {currentStepData.explanation}

-
-
-
- - {/* Current Status */} - {currentStep === storySteps.length - 1 && ( - - - -
- {fixture.result === "allowed" ? ( - - ) : ( - - )} -
-

{fixture.result}

-

- {fixture.result === "allowed" - ? "This change can proceed safely" - : "This change needs more approvals"} -

-
-
-
    - {fixture.reasons.map((reason, i) => ( -
  • - - {reason} -
  • - ))} -
-
-
-
- )} - - {/* Try It CTA */} - - - -
- - {/* Right: Interactive Graph */} - -
-

Trust Flow Visualization

- - Interactive Graph - -
- -
- -
- - {/* Legend */} -
-
-
- Roles -
-
-
- People -
-
-
- Satisfied -
-
-
- Unmet -
-
- -
- - {/* Footer Navigation */} -
- - -
- {storySteps.map((_, index) => ( -
- - -
- - {/* Keyboard Shortcuts */} -
- - ←→ Navigate - - - Space Auto-play - - - Esc Close - -
- - - )} - - ) -} diff --git a/frontend/archive/playground/fixtures/fixture-allowed.json b/frontend/archive/playground/fixtures/fixture-allowed.json deleted file mode 100644 index 3eeb51e..0000000 --- a/frontend/archive/playground/fixtures/fixture-allowed.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "result": "allowed", - "reasons": [ - "All approval requirements satisfied", - "Rule 'protect-main-branch': 2/2 maintainer approvals collected", - "All signatures verified successfully", - "Change is safe to merge" - ], - "approval_requirements": [ - { - "role": "maintainer", - "role_metadata_version": 1, - "threshold": 2, - "file_globs": ["src/**", "docs/**", "*.md"], - "eligible_signers": [ - { - "id": "alice", - "display_name": "Alice Johnson", - "keyid": "ssh-rsa-abc123", - "key_type": "ssh" - }, - { - "id": "bob", - "display_name": "Bob Smith", - "keyid": "gpg-def456", - "key_type": "gpg" - }, - { - "id": "charlie", - "display_name": "Charlie Brown", - "keyid": "sigstore-ghi789", - "key_type": "sigstore" - } - ], - "satisfied": 2, - "satisfiers": [ - { - "who": "alice", - "keyid": "ssh-rsa-abc123", - "signature_valid": true, - "signature_time": "2024-01-15T10:30:00Z", - "signature_verification_reason": "Valid SSH signature from authorized maintainer key" - }, - { - "who": "bob", - "keyid": "gpg-def456", - "signature_valid": true, - "signature_time": "2024-01-15T11:15:00Z", - "signature_verification_reason": "Valid GPG signature with trusted maintainer key" - } - ] - } - ], - "signature_verification": [ - { - "signature_id": "sig-001", - "keyid": "ssh-rsa-abc123", - "sig_ok": true, - "verified_at": "2024-01-15T10:30:00Z", - "reason": "Valid SSH signature from authorized key - signature verified against known public key" - }, - { - "signature_id": "sig-002", - "keyid": "gpg-def456", - "sig_ok": true, - "verified_at": "2024-01-15T11:15:00Z", - "reason": "Valid GPG signature with trusted key - full certificate chain verified" - } - ], - "attestation_matches": [ - { - "attestation_id": "att-001", - "rsl_index": 42, - "maps_to_proposal": true, - "from_revision_ok": true, - "target_tree_hash_match": true, - "signature_valid": true - } - ], - "visualization_hint": { - "nodes": [ - { - "id": "maintainer-role", - "type": "role", - "label": "Maintainer (2/2)", - "meta": { - "satisfied": true, - "threshold": 2, - "current": 2, - "description": "Protects core source code and documentation" - } - }, - { - "id": "alice", - "type": "person", - "label": "Alice Johnson", - "meta": { - "signed": true, - "keyType": "ssh", - "role": "maintainer" - } - }, - { - "id": "bob", - "type": "person", - "label": "Bob Smith", - "meta": { - "signed": true, - "keyType": "gpg", - "role": "maintainer" - } - }, - { - "id": "charlie", - "type": "person", - "label": "Charlie Brown", - "meta": { - "signed": false, - "keyType": "sigstore", - "role": "maintainer" - } - } - ], - "edges": [ - { - "from": "alice", - "to": "maintainer-role", - "label": "Approved ✓", - "satisfied": true - }, - { - "from": "bob", - "to": "maintainer-role", - "label": "Approved ✓", - "satisfied": true - }, - { - "from": "charlie", - "to": "maintainer-role", - "label": "Eligible", - "satisfied": false - } - ] - } -} diff --git a/frontend/archive/playground/fixtures/fixture-blocked.json b/frontend/archive/playground/fixtures/fixture-blocked.json deleted file mode 100644 index cda13aa..0000000 --- a/frontend/archive/playground/fixtures/fixture-blocked.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "result": "blocked", - "reasons": [ - "Rule 'protect-main-branch' requires 2 maintainer approvals but only has 1", - "Rule 'protect-test-files' requires 1 reviewer approval but has 0", - "Missing required signatures prevent this change from being merged" - ], - "approval_requirements": [ - { - "role": "maintainer", - "role_metadata_version": 1, - "threshold": 2, - "file_globs": ["src/**", "docs/**", "*.md"], - "eligible_signers": [ - { - "id": "alice", - "display_name": "Alice Johnson", - "keyid": "ssh-rsa-abc123", - "key_type": "ssh" - }, - { - "id": "bob", - "display_name": "Bob Smith", - "keyid": "gpg-def456", - "key_type": "gpg" - }, - { - "id": "charlie", - "display_name": "Charlie Brown", - "keyid": "sigstore-ghi789", - "key_type": "sigstore" - } - ], - "satisfied": 1, - "satisfiers": [ - { - "who": "alice", - "keyid": "ssh-rsa-abc123", - "signature_valid": true, - "signature_time": "2024-01-15T10:30:00Z", - "signature_verification_reason": "Valid SSH signature from authorized maintainer key" - } - ] - }, - { - "role": "reviewer", - "role_metadata_version": 1, - "threshold": 1, - "file_globs": ["tests/**", "*.test.js", "spec/**"], - "eligible_signers": [ - { - "id": "dave", - "display_name": "Dave Wilson", - "keyid": "ssh-rsa-xyz789", - "key_type": "ssh" - }, - { - "id": "eve", - "display_name": "Eve Martinez", - "keyid": "gpg-uvw456", - "key_type": "gpg" - } - ], - "satisfied": 0, - "satisfiers": [] - } - ], - "signature_verification": [ - { - "signature_id": "sig-001", - "keyid": "ssh-rsa-abc123", - "sig_ok": true, - "verified_at": "2024-01-15T10:30:00Z", - "reason": "Valid SSH signature from authorized key - signature verified against known public key" - } - ], - "attestation_matches": [ - { - "attestation_id": "att-001", - "rsl_index": 42, - "maps_to_proposal": true, - "from_revision_ok": false, - "target_tree_hash_match": true, - "signature_valid": true - } - ], - "visualization_hint": { - "nodes": [ - { - "id": "maintainer-role", - "type": "role", - "label": "Maintainer (1/2)", - "meta": { - "satisfied": false, - "threshold": 2, - "current": 1, - "description": "Protects core source code and documentation" - } - }, - { - "id": "reviewer-role", - "type": "role", - "label": "Reviewer (0/1)", - "meta": { - "satisfied": false, - "threshold": 1, - "current": 0, - "description": "Reviews test files and specifications" - } - }, - { - "id": "alice", - "type": "person", - "label": "Alice Johnson", - "meta": { - "signed": true, - "keyType": "ssh", - "role": "maintainer" - } - }, - { - "id": "bob", - "type": "person", - "label": "Bob Smith", - "meta": { - "signed": false, - "keyType": "gpg", - "role": "maintainer" - } - }, - { - "id": "charlie", - "type": "person", - "label": "Charlie Brown", - "meta": { - "signed": false, - "keyType": "sigstore", - "role": "maintainer" - } - }, - { - "id": "dave", - "type": "person", - "label": "Dave Wilson", - "meta": { - "signed": false, - "keyType": "ssh", - "role": "reviewer" - } - }, - { - "id": "eve", - "type": "person", - "label": "Eve Martinez", - "meta": { - "signed": false, - "keyType": "gpg", - "role": "reviewer" - } - } - ], - "edges": [ - { - "from": "alice", - "to": "maintainer-role", - "label": "Approved ✓", - "satisfied": true - }, - { - "from": "bob", - "to": "maintainer-role", - "label": "Eligible", - "satisfied": false - }, - { - "from": "charlie", - "to": "maintainer-role", - "label": "Eligible", - "satisfied": false - }, - { - "from": "dave", - "to": "reviewer-role", - "label": "Required", - "satisfied": false - }, - { - "from": "eve", - "to": "reviewer-role", - "label": "Eligible", - "satisfied": false - } - ] - } -} diff --git a/frontend/archive/playground/hooks/use-gittuf-simulator.ts b/frontend/archive/playground/hooks/use-gittuf-simulator.ts deleted file mode 100644 index 578076f..0000000 --- a/frontend/archive/playground/hooks/use-gittuf-simulator.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { useState, useCallback, useMemo, useEffect } from "react" -import type { SimulatorResponse, ApprovalRequirement, EligibleSigner, CustomConfig, CustomPerson, CustomRole } from "@/lib/simulator-types" -import fixtureAllowed from "@/fixtures/fixture-allowed.json" -import fixtureBlocked from "@/fixtures/fixture-blocked.json" - -const DEFAULT_CONFIG: CustomConfig = { - people: [ - { - id: "alice", - display_name: "Alice Johnson", - keyid: "ssh-rsa-abc123", - key_type: "ssh", - has_signed: false, - }, - { - id: "bob", - display_name: "Bob Smith", - keyid: "gpg-def456", - key_type: "gpg", - has_signed: false, - }, - { - id: "charlie", - display_name: "Charlie Brown", - keyid: "sigstore-ghi789", - key_type: "sigstore", - has_signed: false, - }, - ], - roles: [ - { - id: "maintainer", - display_name: "Maintainer", - threshold: 2, - file_globs: ["src/**", "docs/**"], - assigned_people: ["alice", "bob", "charlie"], - }, - { - id: "reviewer", - display_name: "Reviewer", - threshold: 1, - file_globs: ["tests/**"], - assigned_people: ["alice", "bob"], - }, - ], -} - -export function useGittufSimulator() { - // Core UI State - const [darkMode, setDarkMode] = useState(false) - const [showStory, setShowStory] = useState(false) - const [showSimulator, setShowSimulator] = useState(false) - const [isProcessing, setIsProcessing] = useState(false) - - // Simulator State - const [currentFixture, setCurrentFixture] = useState<"blocked" | "allowed" | "custom">("blocked") - const [whatIfMode, setWhatIfMode] = useState(false) - const [simulatedSigners, setSimulatedSigners] = useState>(new Set()) - - // UI Layout State - const [expandedGraph, setExpandedGraph] = useState(false) - const [showControls, setShowControls] = useState(true) - const [showDetails, setShowDetails] = useState(false) - - // Custom Config State - const [showCustomConfig, setShowCustomConfig] = useState(false) - const [customConfig, setCustomConfig] = useState(DEFAULT_CONFIG) - - // Form States - const [newPersonForm, setNewPersonForm] = useState<{ - id: string - display_name: string - keyid: string - key_type: "ssh" | "gpg" | "sigstore" - has_signed: boolean - }>({ - id: "", - display_name: "", - keyid: "", - key_type: "ssh", - has_signed: false, - }) - - const [newRoleForm, setNewRoleForm] = useState({ - id: "", - display_name: "", - threshold: 1, - file_globs: ["src/**"], - assigned_people: [] as string[], - }) - - const [editingPerson, setEditingPerson] = useState(null) - const [editingRole, setEditingRole] = useState(null) - - // Generate custom fixture from config - const customFixture = useMemo((): SimulatorResponse => { - const approval_requirements: ApprovalRequirement[] = customConfig.roles.map((role) => { - const eligible_signers: EligibleSigner[] = role.assigned_people - .map((personId) => { - const person = customConfig.people.find((p) => p.id === personId) - return person - ? { - id: person.id, - display_name: person.display_name, - keyid: person.keyid, - key_type: person.key_type, - } - : null - }) - .filter(Boolean) as EligibleSigner[] - - const satisfiers = eligible_signers - .filter((signer) => { - const person = customConfig.people.find((p) => p.id === signer.id) - return person?.has_signed - }) - .map((signer) => ({ - who: signer.id, - keyid: signer.keyid, - signature_valid: true, - signature_time: new Date().toISOString(), - signature_verification_reason: `Valid ${signer.key_type.toUpperCase()} signature`, - })) - - return { - role: role.id, - role_metadata_version: 1, - threshold: role.threshold, - file_globs: role.file_globs, - eligible_signers, - satisfied: satisfiers.length, - satisfiers, - } - }) - - const allRequirementsMet = approval_requirements.every((req) => req.satisfied >= req.threshold) - - const visualization_hint = { - nodes: [ - ...customConfig.roles.map((role) => ({ - id: role.id, - type: "role" as const, - label: `${role.display_name} (${ - approval_requirements.find((req) => req.role === role.id)?.satisfied || 0 - }/${role.threshold})`, - meta: { - satisfied: (approval_requirements.find((req) => req.role === role.id)?.satisfied || 0) >= role.threshold, - threshold: role.threshold, - current: approval_requirements.find((req) => req.role === role.id)?.satisfied || 0, - }, - })), - ...customConfig.people.map((person) => ({ - id: person.id, - type: "person" as const, - label: person.display_name, - meta: { - signed: person.has_signed, - keyType: person.key_type, - }, - })), - ], - edges: customConfig.roles.flatMap((role) => - role.assigned_people.map((personId) => { - const person = customConfig.people.find((p) => p.id === personId) - return { - from: personId, - to: role.id, - label: person?.has_signed ? "Approved" : "Eligible", - satisfied: person?.has_signed || false, - } - }), - ), - } - - return { - result: allRequirementsMet ? "allowed" : "blocked", - reasons: allRequirementsMet - ? ["All approval requirements satisfied"] - : approval_requirements - .filter((req) => req.satisfied < req.threshold) - .map((req) => `Missing ${req.threshold - req.satisfied} ${req.role} approval(s)`), - approval_requirements, - signature_verification: approval_requirements.flatMap((req) => - req.satisfiers.map((satisfier, index) => ({ - signature_id: `sig-${req.role}-${index + 1}`, - keyid: satisfier.keyid, - sig_ok: satisfier.signature_valid, - verified_at: satisfier.signature_time, - reason: satisfier.signature_verification_reason, - })), - ), - attestation_matches: [ - { - attestation_id: "att-custom-001", - rsl_index: 42, - maps_to_proposal: true, - from_revision_ok: true, - target_tree_hash_match: true, - signature_valid: true, - }, - ], - visualization_hint, - } - }, [customConfig]) - - // Get current fixture - const getCurrentFixture = useCallback((): SimulatorResponse => { - switch (currentFixture) { - case "allowed": - return fixtureAllowed as SimulatorResponse - case "custom": - return customFixture - default: - return fixtureBlocked as SimulatorResponse - } - }, [currentFixture, customFixture]) - - const fixture = getCurrentFixture() - - // Calculate what-if result - const displayResult = useMemo((): SimulatorResponse => { - if (!whatIfMode || simulatedSigners.size === 0) return fixture - - const whatIfResult = JSON.parse(JSON.stringify(fixture)) as SimulatorResponse - - whatIfResult.approval_requirements = whatIfResult.approval_requirements.map((req) => { - const additionalSatisfiers = req.eligible_signers - .filter((signer) => simulatedSigners.has(signer.id) && !req.satisfiers.some((s) => s.who === signer.id)) - .map((signer) => ({ - who: signer.id, - keyid: signer.keyid, - signature_valid: true, - signature_time: new Date().toISOString(), - signature_verification_reason: "Simulated signature", - })) - - return { - ...req, - satisfied: req.satisfied + additionalSatisfiers.length, - satisfiers: [...req.satisfiers, ...additionalSatisfiers], - } - }) - - const allRequirementsMet = whatIfResult.approval_requirements.every((req) => req.satisfied >= req.threshold) - whatIfResult.result = allRequirementsMet ? "allowed" : "blocked" - - if (allRequirementsMet && fixture.result === "blocked") { - whatIfResult.reasons = ["All approval requirements satisfied (with simulated signatures)"] - } - - return whatIfResult - }, [whatIfMode, simulatedSigners, fixture]) - - // Event Handlers - const handleRunSimulation = useCallback(async () => { - setIsProcessing(true) - setShowSimulator(true) - await new Promise((resolve) => setTimeout(resolve, 1000)) - setIsProcessing(false) - }, []) - - const handleSimulatedSignerToggle = useCallback((signerId: string, checked: boolean) => { - setSimulatedSigners((prev) => { - const newSet = new Set(prev) - if (checked) { - newSet.add(signerId) - } else { - newSet.delete(signerId) - } - return newSet - }) - }, []) - - const handleExportJson = useCallback(() => { - const dataStr = JSON.stringify(displayResult, null, 2) - const dataBlob = new Blob([dataStr], { type: "application/json" }) - const url = URL.createObjectURL(dataBlob) - const link = document.createElement("a") - link.href = url - link.download = `gittuf-simulation-${Date.now()}.json` - link.click() - URL.revokeObjectURL(url) - }, [displayResult]) - - const addPerson = useCallback(() => { - if (!newPersonForm.id || !newPersonForm.display_name) return - - const newPerson = { - ...newPersonForm, - keyid: newPersonForm.keyid || `${newPersonForm.key_type}-${Date.now()}`, - } - - setCustomConfig((prev) => ({ - ...prev, - people: [...prev.people, newPerson], - })) - - setNewPersonForm({ - id: "", - display_name: "", - keyid: "", - key_type: "ssh", - has_signed: false, - }) - }, [newPersonForm]) - - const addRole = useCallback(() => { - if (!newRoleForm.id || !newRoleForm.display_name) return - - setCustomConfig((prev) => ({ - ...prev, - roles: [...prev.roles, { ...newRoleForm }], - })) - - setNewRoleForm({ - id: "", - display_name: "", - threshold: 1, - file_globs: ["src/**"], - assigned_people: [], - }) - }, [newRoleForm]) - - const deletePerson = useCallback((id: string) => { - setCustomConfig((prev) => ({ - ...prev, - people: prev.people.filter((p) => p.id !== id), - roles: prev.roles.map((role) => ({ - ...role, - assigned_people: role.assigned_people.filter((pid) => pid !== id), - })), - })) - }, []) - - const deleteRole = useCallback((id: string) => { - setCustomConfig((prev) => ({ - ...prev, - roles: prev.roles.filter((r) => r.id !== id), - })) - }, []) - - const updatePerson = useCallback((person: CustomPerson) => { - setCustomConfig((prev) => ({ - ...prev, - people: prev.people.map((p) => (p.id === person.id ? person : p)), - })) - setEditingPerson(null) - }, []) - - const updateRole = useCallback((role: CustomRole) => { - setCustomConfig((prev) => ({ - ...prev, - roles: prev.roles.map((r) => (r.id === role.id ? role : r)), - })) - setEditingRole(null) - }, []) - - const togglePersonSigned = useCallback((personId: string) => { - setCustomConfig((prev) => ({ - ...prev, - people: prev.people.map((p) => (p.id === personId ? { ...p, has_signed: !p.has_signed } : p)), - })) - }, []) - - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (!e || !e.key || typeof e.key !== "string") return - if (e.ctrlKey || e.metaKey) return - - try { - const key = e.key.toLowerCase() - - switch (key) { - case "r": - e.preventDefault() - handleRunSimulation() - break - case "w": - e.preventDefault() - setWhatIfMode(!whatIfMode) - break - case "s": - e.preventDefault() - setShowStory(true) - break - case "e": - e.preventDefault() - handleExportJson() - break - case "f": - e.preventDefault() - setExpandedGraph(!expandedGraph) - break - case "c": - e.preventDefault() - setShowCustomConfig(!showCustomConfig) - break - } - } catch (error) { - console.warn("Keyboard shortcut error:", error) - } - } - - // Only attach listener if no modal is open (simplified check) - // In a real app we might want more robust context - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [handleRunSimulation, handleExportJson, whatIfMode, expandedGraph, showCustomConfig, setShowStory, setWhatIfMode, setExpandedGraph, setShowCustomConfig]) - - return { - darkMode, setDarkMode, - showStory, setShowStory, - showSimulator, setShowSimulator, - isProcessing, - currentFixture, setCurrentFixture, - whatIfMode, setWhatIfMode, - simulatedSigners, - expandedGraph, setExpandedGraph, - showControls, setShowControls, - showDetails, setShowDetails, - showCustomConfig, setShowCustomConfig, - customConfig, - newPersonForm, setNewPersonForm, - newRoleForm, setNewRoleForm, - editingPerson, setEditingPerson, - editingRole, setEditingRole, - fixture, - displayResult, - handleRunSimulation, - handleSimulatedSignerToggle, - handleExportJson, - addPerson, - addRole, - deletePerson, - deleteRole, - updatePerson, - updateRole, - togglePersonSigned, - customFixture // Exporting just in case, though handled internally - } -} - -export type SimulatorState = ReturnType diff --git a/frontend/archive/playground/lib/simulator-types.ts b/frontend/archive/playground/lib/simulator-types.ts deleted file mode 100644 index dae8618..0000000 --- a/frontend/archive/playground/lib/simulator-types.ts +++ /dev/null @@ -1,99 +0,0 @@ -export interface SimulatorResponse { - result: "allowed" | "blocked" - reasons: string[] - approval_requirements: ApprovalRequirement[] - signature_verification: SignatureVerification[] - attestation_matches: AttestationMatch[] - visualization_hint: VisualizationHint -} - -export interface ApprovalRequirement { - role: string - role_metadata_version: number - threshold: number - file_globs: string[] - eligible_signers: EligibleSigner[] - satisfied: number - satisfiers: Satisfier[] -} - -export interface EligibleSigner { - id: string - display_name: string - keyid: string - key_type: "ssh" | "gpg" | "sigstore" -} - -export interface Satisfier { - who: string - keyid: string - signature_valid: boolean - signature_time: string - signature_verification_reason: string -} - -export interface SignatureVerification { - signature_id: string - keyid: string - sig_ok: boolean - verified_at: string - reason: string -} - -export interface AttestationMatch { - attestation_id: string - rsl_index: number - maps_to_proposal: boolean - from_revision_ok: boolean - target_tree_hash_match: boolean - signature_valid: boolean -} - -export interface VisualizationHint { - nodes: VisualizationNode[] - edges: VisualizationEdge[] -} - -export interface VisualizationNode { - id: string - type: "role" | "person" | "key" - label: string - meta?: Record -} - -export interface VisualizationEdge { - from: string - to: string - label: string - satisfied: boolean -} - -export interface ProposedChange { - type: "ref-update" | "commit" | "pr" - ref?: string - from?: string - to?: string - commit?: string - pr_json?: string -} - -export interface CustomPerson { - id: string - display_name: string - keyid: string - key_type: "ssh" | "gpg" | "sigstore" - has_signed: boolean -} - -export interface CustomRole { - id: string - display_name: string - threshold: number - file_globs: string[] - assigned_people: string[] -} - -export interface CustomConfig { - people: CustomPerson[] - roles: CustomRole[] -} diff --git a/frontend/archive/playground/screens/playground/simulator-analysis.tsx b/frontend/archive/playground/screens/playground/simulator-analysis.tsx deleted file mode 100644 index 82cf2cb..0000000 --- a/frontend/archive/playground/screens/playground/simulator-analysis.tsx +++ /dev/null @@ -1,176 +0,0 @@ -"use client" - -import { ChevronUp, ChevronDown, CheckCircle, XCircle, AlertTriangle } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import type { SimulatorState } from "@/hooks/use-gittuf-simulator" - -interface SimulatorAnalysisProps { - state: SimulatorState -} - -export function SimulatorAnalysis({ state }: SimulatorAnalysisProps) { - const { - expandedGraph, - darkMode, - showDetails, - setShowDetails, - displayResult, - simulatedSigners, - whatIfMode, - } = state - - if (expandedGraph) return null - - return ( - - -
- Detailed Analysis - -
-
- {showDetails && ( - - - - Approvals - Signatures - Attestations - - - -
- {displayResult.approval_requirements.map((req, index) => ( - - -
- {req.role} - = req.threshold ? "default" : "destructive"}> - {req.satisfied}/{req.threshold} - -
-
- -
Files: {req.file_globs.join(", ")}
-
- {req.eligible_signers.map((signer) => { - const hasSigned = req.satisfiers.some((s) => s.who === signer.id) - const isSimulated = simulatedSigners.has(signer.id) - - return ( -
- {hasSigned ? ( - - ) : isSimulated && whatIfMode ? ( - - ) : ( - - )} - {signer.display_name} - - {signer.key_type.toUpperCase()} - - {isSimulated && whatIfMode && ( - - Simulated - - )} -
- ) - })} -
-
-
- ))} -
-
- - -
- {displayResult.signature_verification.map((sig, index) => ( - - -
-
-
{sig.signature_id}
-
Key: {sig.keyid}
-
- Verified: {new Date(sig.verified_at).toLocaleString()} -
- {sig.reason &&
{sig.reason}
} -
- - {sig.sig_ok ? "Valid" : "Invalid"} - -
-
-
- ))} -
-
- - -
- {displayResult.attestation_matches.map((att, index) => ( - - -
-
-
{att.attestation_id}
- RSL #{att.rsl_index} -
- -
-
- {att.maps_to_proposal ? ( - - ) : ( - - )} - Maps to proposal -
-
- {att.from_revision_ok ? ( - - ) : ( - <> - - - - )} - From revision OK -
-
- {att.target_tree_hash_match ? ( - - ) : ( - - )} - Tree hash match -
-
- {att.signature_valid ? ( - - ) : ( - - )} - Signature valid -
-
-
-
-
- ))} -
-
-
-
- )} -
- ) -} diff --git a/frontend/archive/playground/screens/playground/simulator-config-modal.tsx b/frontend/archive/playground/screens/playground/simulator-config-modal.tsx deleted file mode 100644 index 387dec6..0000000 --- a/frontend/archive/playground/screens/playground/simulator-config-modal.tsx +++ /dev/null @@ -1,297 +0,0 @@ -"use client" - -import { UserPlus, Plus, Users, XCircle, CheckCircle, Edit, Trash2, ShieldPlus } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Checkbox } from "@/components/ui/checkbox" -import { Badge } from "@/components/ui/badge" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import type { SimulatorState } from "@/hooks/use-gittuf-simulator" - -interface SimulatorConfigModalProps { - state: SimulatorState -} - -export function SimulatorConfigModal({ state }: SimulatorConfigModalProps) { - const { - showCustomConfig, - setShowCustomConfig, - customConfig, - newPersonForm, - setNewPersonForm, - newRoleForm, - setNewRoleForm, - setEditingPerson, - addPerson, - addRole, - deletePerson, - deleteRole, - togglePersonSigned, - } = state - - return ( - - - - Custom Organization Configuration - - - - People & Signers - Roles & Policies - - - - {/* Add New Person */} - - - - - Add New Person - - - -
{ - e.preventDefault() - addPerson() - }} - className="space-y-3" - > -
-
- - setNewPersonForm({ ...newPersonForm, id: e.target.value })} - placeholder="e.g., john_doe" - /> -
-
- - setNewPersonForm({ ...newPersonForm, display_name: e.target.value })} - placeholder="e.g., John Doe" - /> -
-
-
-
- - setNewPersonForm({ ...newPersonForm, keyid: e.target.value })} - placeholder="Auto-generated if empty" - /> -
-
- - -
-
- -
-
-
- - {/* Existing People */} -
- {customConfig.people.map((person) => ( - - -
-
-
- - {person.display_name} - {person.key_type.toUpperCase()} - {person.has_signed && Signed} -
-
-
- - - -
-
-
- ID: {person.id} | Key: {person.keyid} -
-
-
- ))} -
-
- - - {/* Add New Role */} - - - - - Add New Role - - - -
{ - e.preventDefault() - addRole() - }} - className="space-y-3" - > -
-
- - setNewRoleForm({ ...newRoleForm, id: e.target.value })} - placeholder="e.g., maintainer" - /> -
-
- - setNewRoleForm({ ...newRoleForm, display_name: e.target.value })} - placeholder="e.g., Maintainer" - /> -
-
-
-
- - - setNewRoleForm({ ...newRoleForm, threshold: Number.parseInt(e.target.value) || 1 }) - } - /> -
-
- - - setNewRoleForm({ - ...newRoleForm, - file_globs: e.target.value.split(",").map((s) => s.trim()), - }) - } - placeholder="e.g., src/**, docs/**" - /> -
-
-
- -
- {customConfig.people.map((person) => ( -
- { - if (checked) { - setNewRoleForm({ - ...newRoleForm, - assigned_people: [...newRoleForm.assigned_people, person.id], - }) - } else { - setNewRoleForm({ - ...newRoleForm, - assigned_people: newRoleForm.assigned_people.filter((id) => id !== person.id), - }) - } - }} - /> - -
- ))} -
-
- -
-
-
- - {/* Existing Roles */} -
- {customConfig.roles.map((role) => ( - - -
-
- - {role.display_name} - Threshold: {role.threshold} -
- -
-
-
Files: {role.file_globs.join(", ")}
-
- Assigned:{" "} - {role.assigned_people - .map((pid) => customConfig.people.find((p) => p.id === pid)?.display_name) - .filter(Boolean) - .join(", ")} -
-
-
-
- ))} -
-
-
-
-
- ) -} diff --git a/frontend/archive/playground/screens/playground/simulator-controls.tsx b/frontend/archive/playground/screens/playground/simulator-controls.tsx deleted file mode 100644 index 2c05fbd..0000000 --- a/frontend/archive/playground/screens/playground/simulator-controls.tsx +++ /dev/null @@ -1,125 +0,0 @@ -"use client" - -import { motion } from "framer-motion" -import { Settings, ChevronUp, ChevronDown, Download, Users } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Label } from "@/components/ui/label" -import { Switch } from "@/components/ui/switch" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Checkbox } from "@/components/ui/checkbox" -import type { SimulatorState } from "@/hooks/use-gittuf-simulator" - -interface SimulatorControlsProps { - state: SimulatorState -} - -export function SimulatorControls({ state }: SimulatorControlsProps) { - const { - expandedGraph, - showControls, - setShowControls, - darkMode, - whatIfMode, - setWhatIfMode, - currentFixture, - setCurrentFixture, - displayResult, - simulatedSigners, - handleSimulatedSignerToggle, - handleExportJson, - } = state - - if (expandedGraph) return null - - return ( - - - -
- - - Controls - - -
-
- {showControls && ( - - {/* What-If Toggle */} -
- - -
- - {/* Scenario Selection */} -
- - -
- - {/* What-If Signers */} - {whatIfMode && ( - - -
- {displayResult.approval_requirements.map((req) => - req.eligible_signers - .filter((signer) => !req.satisfiers.some((s) => s.who === signer.id)) - .map((signer) => ( -
- - handleSimulatedSignerToggle(signer.id, checked as boolean) - } - /> - -
- )) - )} -
-
- )} - - {/* Export Actions */} -
- -
-
- )} -
-
- ) -} diff --git a/frontend/archive/playground/screens/playground/simulator-glossary.tsx b/frontend/archive/playground/screens/playground/simulator-glossary.tsx deleted file mode 100644 index 36d9626..0000000 --- a/frontend/archive/playground/screens/playground/simulator-glossary.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client" - -import { motion } from "framer-motion" -import { Crown, Shield, FileText, Users } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import type { SimulatorState } from "@/hooks/use-gittuf-simulator" - -interface SimulatorGlossaryProps { - state: SimulatorState -} - -export function SimulatorGlossary({ state }: SimulatorGlossaryProps) { - const { darkMode } = state - - return ( - - - - Glossary - - -
-
-

- - Root -

-

The ultimate authority that can delegate permissions

-
-
-

- - Role -

-

A permission set that protects specific files or operations

-
-
-

- - Attestation -

-

A signed approval tied to a specific change

-
-
-

- - Threshold -

-

Minimum number of signatures required for approval

-
-
-
-
-
- ) -} diff --git a/frontend/archive/playground/screens/playground/simulator-graph.tsx b/frontend/archive/playground/screens/playground/simulator-graph.tsx deleted file mode 100644 index 39cbc0b..0000000 --- a/frontend/archive/playground/screens/playground/simulator-graph.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client" - -import { motion } from "framer-motion" -import { Sparkles, Maximize2, Minimize2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { TrustGraph } from "@/screens/playground/trust-graph" -import type { SimulatorState } from "@/hooks/use-gittuf-simulator" - -interface SimulatorGraphProps { - state: SimulatorState -} - -export function SimulatorGraph({ state }: SimulatorGraphProps) { - const { - expandedGraph, - setExpandedGraph, - darkMode, - displayResult, - whatIfMode, - simulatedSigners, - currentFixture, - customConfig, - isProcessing, - handleSimulatedSignerToggle, - } = state - - return ( - - - -
- - - Trust Graph Visualization - {whatIfMode && ( - - Interactive Mode - - )} - {currentFixture === "custom" && ( - - Custom Config - - )} - -
- {simulatedSigners.size > 0 && ( - - {simulatedSigners.size} simulated - - )} - -
-
-
- -
- { - const node = displayResult.visualization_hint.nodes.find((n) => n.id === nodeId) - if (node?.type === "person" && whatIfMode) { - handleSimulatedSignerToggle(nodeId, !simulatedSigners.has(nodeId)) - } - }} - /> -
- {whatIfMode && ( -
-

- 💡 Click on person nodes in the graph to simulate their signatures -

-
- )} - {currentFixture === "custom" && ( -
-

- 🎯 Using your custom organization configuration with {customConfig.people.length} people and{" "} - {customConfig.roles.length} roles -

-
- )} -
-
-
- ) -} diff --git a/frontend/archive/playground/screens/playground/simulator-header.tsx b/frontend/archive/playground/screens/playground/simulator-header.tsx deleted file mode 100644 index 52f91d8..0000000 --- a/frontend/archive/playground/screens/playground/simulator-header.tsx +++ /dev/null @@ -1,117 +0,0 @@ -"use client" - -import { motion } from "framer-motion" -import { Shield, Sun, Moon, BookOpen, Zap } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import type { SimulatorState } from "@/hooks/use-gittuf-simulator" - -interface SimulatorHeaderProps { - state: SimulatorState -} - -export function SimulatorHeader({ state }: SimulatorHeaderProps) { - const { - darkMode, - setDarkMode, - currentFixture, - setCurrentFixture, - setShowStory, - handleRunSimulation, - isProcessing, - } = state - - return ( - -
-
- - - -
-

- Policy Verification Simulator -

-

- Test whether gittuf policy permits your proposed changes -

-
-
- -
- - -
-
- -
- - -
- -
- - R Run - - - W What-If - - - S Story - - - C Config - - - F Fullscreen - -
-
- ) -} diff --git a/frontend/archive/playground/screens/playground/trust-graph.tsx b/frontend/archive/playground/screens/playground/trust-graph.tsx deleted file mode 100644 index b26a722..0000000 --- a/frontend/archive/playground/screens/playground/trust-graph.tsx +++ /dev/null @@ -1,626 +0,0 @@ -"use client" - -import { useEffect, useRef, useState, useCallback, useMemo } from "react" -import { motion } from "framer-motion" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { ZoomIn, ZoomOut, RotateCcw, Users, Shield, Key } from "lucide-react" -import type { Core, NodeSingular } from "cytoscape" -import type { VisualizationHint } from "@/lib/simulator-types" - -interface ApprovalRequirement { - role: string - threshold: number - satisfied: number - satisfiers: Array<{ who: string }> -} - -interface CyElement { - data: (key: string) => string | number | boolean | undefined -} - -interface CyEvent { - target: { - data: (key: string) => string | number | boolean | undefined - } -} - -interface LegendItem { - key: string - type: string - label: string - status: string -} - -interface Satisfier { - who: string -} - - -interface TrustGraphProps { - hint: VisualizationHint - className?: string - focusedNodes?: string[] - animatePulse?: boolean - onNodeClick?: (nodeId: string) => void - simulatedSigners?: Set - approvalRequirements?: ApprovalRequirement[] -} - -export function TrustGraph({ - hint, - className = "", - focusedNodes = [], - animatePulse = false, - onNodeClick, - simulatedSigners = new Set(), - approvalRequirements = [], -}: TrustGraphProps) { - const containerRef = useRef(null) - const cyRef = useRef(null) - const [selectedNode, setSelectedNode] = useState(null) - const selectedNodeRef = useRef(selectedNode) - const [isLoading, setIsLoading] = useState(true) - const [layoutApplied, setLayoutApplied] = useState(false) - const [graphKey, setGraphKey] = useState(0) - - useEffect(() => { - selectedNodeRef.current = selectedNode - }, [selectedNode]) - - // Create enhanced nodes with status information - memoized to prevent unnecessary recalculations - const enhancedNodes = useMemo(() => { - return hint.nodes.map((node) => { - let status = "inactive" - let satisfied = false - - if (node.type === "person") { - // Check if this person has signed - const hasSigned = approvalRequirements.some((req) => req.satisfiers.some((s) => s.who === node.id)) - const isSimulated = simulatedSigners.has(node.id) - - if (hasSigned) { - status = "signed" - satisfied = true - } else if (isSimulated) { - status = "simulated" - satisfied = true - } else { - status = "pending" - } - } else if (node.type === "role") { - // Check if role requirements are satisfied - const roleReq = approvalRequirements.find((req) => req.role === node.id) - if (roleReq) { - satisfied = roleReq.satisfied >= roleReq.threshold - status = satisfied ? "satisfied" : "unsatisfied" - } - } - - return { - ...node, - status, - satisfied, - } - }) - }, [hint.nodes, approvalRequirements, simulatedSigners]) - - const getNodeColor = useCallback((type: string, status: string, isFocused: boolean) => { - if (type === "role") { - if (status === "satisfied") return isFocused ? "#059669" : "#10b981" - if (status === "unsatisfied") return isFocused ? "#dc2626" : "#ef4444" - return isFocused ? "#3b82f6" : "#6b7280" - } else if (type === "person") { - if (status === "signed") return isFocused ? "#059669" : "#10b981" - if (status === "simulated") return isFocused ? "#0284c7" : "#0ea5e9" - if (status === "pending") return isFocused ? "#d97706" : "#f59e0b" - return isFocused ? "#7c3aed" : "#8b5cf6" - } - return isFocused ? "#374151" : "#6b7280" - }, []) - - // Calculate initial positions for hierarchical layout - const calculateInitialPositions = useCallback(() => { - const positions: { [key: string]: { x: number; y: number } } = {} - const container = containerRef.current - if (!container) return positions - - const containerWidth = container.offsetWidth || 800 - - - // Separate nodes by type - const roleNodes = enhancedNodes.filter((n) => n.type === "role") - const personNodes = enhancedNodes.filter((n) => n.type === "person") - const otherNodes = enhancedNodes.filter((n) => n.type !== "role" && n.type !== "person") - - // Position role nodes in the top tier - const roleY = 120 - const roleSpacing = Math.min(200, (containerWidth - 200) / Math.max(1, roleNodes.length - 1)) - const roleStartX = (containerWidth - (roleNodes.length - 1) * roleSpacing) / 2 - - roleNodes.forEach((node, index) => { - positions[node.id] = { - x: roleStartX + index * roleSpacing, - y: roleY, - } - }) - - // Position person nodes in lower tiers based on their role connections - const personY = 320 - const personSpacing = Math.min(150, (containerWidth - 200) / Math.max(1, personNodes.length - 1)) - const personStartX = (containerWidth - (personNodes.length - 1) * personSpacing) / 2 - - personNodes.forEach((node, index) => { - // Try to position people below their connected roles - const connectedRoles = hint.edges - .filter((edge) => edge.from === node.id) - .map((edge) => edge.to) - .filter((roleId) => roleNodes.some((r) => r.id === roleId)) - - if (connectedRoles.length > 0) { - // Position below the average x of connected roles - const avgX = - connectedRoles.reduce((sum, roleId) => { - const rolePos = positions[roleId] - return sum + (rolePos ? rolePos.x : personStartX + index * personSpacing) - }, 0) / connectedRoles.length - - positions[node.id] = { - x: avgX + (Math.random() - 0.5) * 60, // Add slight randomness - y: personY + (index % 2) * 80, // Stagger vertically - } - } else { - // Default positioning - positions[node.id] = { - x: personStartX + index * personSpacing, - y: personY, - } - } - }) - - // Position other nodes - otherNodes.forEach((node, index) => { - positions[node.id] = { - x: 100 + index * 150, - y: 500, - } - }) - - return positions - }, [enhancedNodes, hint.edges]) - - const initializeGraph = useCallback(async () => { - if (!containerRef.current || typeof window === "undefined") return - - const container = containerRef.current - if (!container || !container.offsetWidth || !container.offsetHeight) { - console.warn("Container not ready for graph initialization") - return - } - - setIsLoading(true) - setLayoutApplied(false) - - try { - const cytoscape = (await import("cytoscape")).default - - if (cyRef.current) { - cyRef.current.destroy() - } - - const initialPositions = calculateInitialPositions() - - const cy = cytoscape({ - container: containerRef.current, - elements: [ - ...enhancedNodes.map((node) => ({ - data: { - id: node.id, - label: node.label, - type: node.type, - status: node.status, - satisfied: node.satisfied, - ...node.meta, - }, - position: initialPositions[node.id] || { x: 100, y: 100 }, - })), - ...hint.edges.map((edge) => ({ - data: { - id: `${edge.from}-${edge.to}`, - source: edge.from, - target: edge.to, - label: edge.label, - satisfied: edge.satisfied, - }, - })), - ], - style: [ - { - selector: "node", - style: { - "background-color": (ele: CyElement) => { - const type = ele.data("type") as string - const status = ele.data("status") as string - const isFocused = focusedNodes.includes(ele.data("id") as string) - return getNodeColor(type, status, isFocused) - }, - label: "data(label)", - "text-valign": "center", - "text-halign": "center", - color: "#ffffff", - "font-size": "12px", - "font-weight": 600, - "text-wrap": "wrap", - "text-max-width": "80px", - width: (ele: CyElement) => { - const type = ele.data("type") - return type === "role" ? "80px" : "60px" - }, - height: (ele: CyElement) => { - const type = ele.data("type") - return type === "role" ? "80px" : "60px" - }, - shape: (ele: CyElement) => { - const type = ele.data("type") - if (type === "role") return "hexagon" - if (type === "person") return "ellipse" - return "rectangle" - }, - "border-width": (ele: CyElement) => { - const isFocused = focusedNodes.includes(ele.data("id") as string) - const isSelected = selectedNodeRef.current === (ele.data("id") as string) - return isSelected ? "4px" : isFocused ? "3px" : "2px" - }, - "border-color": (ele: CyElement) => { - const isSelected = selectedNodeRef.current === (ele.data("id") as string) - const isFocused = focusedNodes.includes(ele.data("id") as string) - if (isSelected) return "#fbbf24" - if (isFocused) return "#ffffff" - return "rgba(255, 255, 255, 0.8)" - }, - "transition-property": "background-color, border-color, border-width", - "transition-duration": 300, - }, - }, - { - selector: "edge", - style: { - width: (ele: CyElement) => (ele.data("satisfied") ? "4px" : "2px"), - "line-color": (ele: CyElement) => (ele.data("satisfied") ? "#10b981" : "#ef4444"), - "target-arrow-color": (ele: CyElement) => (ele.data("satisfied") ? "#10b981" : "#ef4444"), - "target-arrow-shape": "triangle", - - "curve-style": "bezier", - "control-point-step-size": 60, - label: "data(label)", - "font-size": "10px", - "font-weight": 600, - color: "#374151", - "text-background-color": "#ffffff", - "text-background-opacity": 0.9, - "text-background-padding": "4px", - "text-background-shape": "roundrectangle", - "line-style": (ele: CyElement) => (ele.data("satisfied") ? "solid" : "dashed"), - "transition-property": "line-color, target-arrow-color, width", - "transition-duration": 300, - }, - }, - ], - zoomingEnabled: true, - userZoomingEnabled: true, - panningEnabled: true, - userPanningEnabled: true, - boxSelectionEnabled: false, - selectionType: "single", - minZoom: 0.2, - maxZoom: 3, - }) - - // Event handlers - cy.on("tap", "node", (evt: CyEvent) => { - const nodeId = evt.target.data("id") as string - setSelectedNode(nodeId) - onNodeClick?.(nodeId) - }) - - cy.on("tap", (evt: CyEvent) => { - if (evt.target === cy) { - setSelectedNode(null) - } - }) - - cyRef.current = cy - - // Fit the graph to the container with some padding - setTimeout(() => { - if (cyRef.current) { - cyRef.current.fit(undefined, 50) - setLayoutApplied(true) - setIsLoading(false) - } - }, 200) - - // Simple pulse animation - if (animatePulse) { - setTimeout(() => { - const pulseNodes = () => { - if (!cyRef.current) return - - enhancedNodes.forEach((node, index) => { - setTimeout(() => { - if (cyRef.current) { - const cyNode = cyRef.current.getElementById(node.id) - if (cyNode && cyNode.length > 0) { - cyNode.style({ - "border-width": "6px", - "border-color": "#fbbf24", - }) - - setTimeout(() => { - if (cyRef.current) { - const cyNodeReset = cyRef.current.getElementById(node.id) - if (cyNodeReset && cyNodeReset.length > 0) { - cyNodeReset.style({ - "border-width": "2px", - "border-color": "rgba(255, 255, 255, 0.8)", - }) - } - } - }, 600) - } - } - }, index * 200) - }) - } - - pulseNodes() - }, 800) - } - } catch (error) { - console.error("Failed to initialize graph:", error) - setIsLoading(false) - } - }, [ - enhancedNodes, - hint.edges, - getNodeColor, - calculateInitialPositions, - animatePulse, - onNodeClick, - focusedNodes, - ]) - - // Force re-initialization when hint changes significantly - useEffect(() => { - setGraphKey((prev) => prev + 1) - initializeGraph() - - return () => { - if (cyRef.current) { - cyRef.current.destroy() - cyRef.current = null - } - } - }, [hint.nodes.length, hint.edges.length, initializeGraph]) - - // Separate effect for updating existing graph without re-creating it - useEffect(() => { - if (!cyRef.current || !layoutApplied) return - - try { - // Update node styles without recreating the graph - cyRef.current.nodes().forEach((node: NodeSingular) => { - const nodeData = enhancedNodes.find((n) => n.id === node.data("id")) - if (nodeData) { - node.data("status", nodeData.status) - node.data("satisfied", nodeData.satisfied) - - // Update the background color directly - const isFocused = focusedNodes.includes(node.data("id")) - const isSelected = selectedNode === node.data("id") - - const newColor = getNodeColor(nodeData.type, nodeData.status, isFocused) - node.style("background-color", newColor) - - // Update border for selection/focus - node.style({ - "border-width": isSelected ? "4px" : isFocused ? "3px" : "2px", - "border-color": isSelected ? "#fbbf24" : isFocused ? "#ffffff" : "rgba(255, 255, 255, 0.8)" - }) - } - }) - } catch (error) { - console.error("Failed to update graph:", error) - } - }, [simulatedSigners, approvalRequirements, enhancedNodes, focusedNodes, getNodeColor, layoutApplied, selectedNode]) - - useEffect(() => { - if (!containerRef.current) return - - const resizeObserver = new ResizeObserver(() => { - if (cyRef.current && layoutApplied) { - setTimeout(() => { - if (cyRef.current) { - cyRef.current.fit(undefined, 50) - } - }, 100) - } - }) - - resizeObserver.observe(containerRef.current) - - return () => { - resizeObserver.disconnect() - } - }, [layoutApplied]) - - const handleZoomIn = () => { - if (cyRef.current) { - cyRef.current.zoom(cyRef.current.zoom() * 1.2) - } - } - - const handleZoomOut = () => { - if (cyRef.current) { - cyRef.current.zoom(cyRef.current.zoom() * 0.8) - } - } - - const handleReset = () => { - if (cyRef.current) { - cyRef.current.fit(undefined, 40) - setSelectedNode(null) - } - } - - const getNodeIcon = (type: string) => { - switch (type) { - case "role": - return - case "person": - return - case "key": - return - default: - return null - } - } - - const getStatusColor = (status: string) => { - switch (status) { - case "signed": - return "bg-green-500" - case "simulated": - return "bg-blue-500" - case "satisfied": - return "bg-green-500" - case "pending": - return "bg-yellow-500" - case "unsatisfied": - return "bg-red-500" - default: - return "bg-gray-500" - } - } - - return ( -
- {/* Loading overlay */} - {isLoading && ( -
- - Building trust graph... -
- )} - - {/* Graph container */} -
- - {/* Controls */} -
- - - -
- - {/* Legend */} -
-

Legend

-
- {enhancedNodes - .reduce((acc, node) => { - const key = `${node.type}-${node.status}` - if (!acc.some((item) => item.key === key)) { - acc.push({ - key, - type: node.type, - status: node.status, - label: - node.status === "signed" - ? "Signed" - : node.status === "simulated" - ? "Simulated" - : node.status === "satisfied" - ? "Satisfied" - : node.status === "pending" - ? "Pending" - : node.status === "unsatisfied" - ? "Unsatisfied" - : "Inactive", - }) - } - return acc - }, [] as LegendItem[]) - .map((item) => ( -
-
- - {getNodeIcon(item.type)} - {item.label} - -
- ))} -
-
- - {/* Selected node info */} - {selectedNode && ( - - {(() => { - const node = enhancedNodes.find((n) => n.id === selectedNode) - if (!node) return null - - return ( -
-
- {getNodeIcon(node.type)} - {node.label} - - {node.status} - -
-
-
Type: {node.type}
- {node.type === "person" && ( -
- {simulatedSigners.has(node.id) - ? "Simulated signer" - : approvalRequirements.some((req) => req.satisfiers.some((s: Satisfier) => s.who === node.id)) - ? "Has signed" - : "Eligible signer"} -
- )} - {node.type === "role" && ( -
- {(() => { - const roleReq = approvalRequirements.find((req) => req.role === node.id) - return roleReq ? `${roleReq.satisfied}/${roleReq.threshold} signatures` : "Role requirements" - })()} -
- )} -
-
- ) - })()} -
- )} -
- ) -} diff --git a/frontend/components/app/header.tsx b/frontend/components/app/header.tsx index 3d381d0..9069e04 100644 --- a/frontend/components/app/header.tsx +++ b/frontend/components/app/header.tsx @@ -18,7 +18,7 @@ export default function Header({ return (
- gittuf + gittuf
{hasCommits && ( diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx deleted file mode 100644 index 55c2f6e..0000000 --- a/frontend/components/theme-provider.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ThemeProvider as NextThemesProvider, - type ThemeProviderProps, -} from 'next-themes' - -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} -} diff --git a/frontend/components/ui/accordion.tsx b/frontend/components/ui/accordion.tsx deleted file mode 100644 index 24c788c..0000000 --- a/frontend/components/ui/accordion.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDown } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Accordion = AccordionPrimitive.Root - -const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AccordionItem.displayName = "AccordionItem" - -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)) - -AccordionContent.displayName = AccordionPrimitive.Content.displayName - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/frontend/components/ui/alert-dialog.tsx b/frontend/components/ui/alert-dialog.tsx deleted file mode 100644 index 25e7b47..0000000 --- a/frontend/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,141 +0,0 @@ -"use client" - -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger - -const AlertDialogPortal = AlertDialogPrimitive.Portal - -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName - -const AlertDialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" - -const AlertDialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" - -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName - -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName - -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName - -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx deleted file mode 100644 index 41fa7e0..0000000 --- a/frontend/components/ui/alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" - -export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/components/ui/aspect-ratio.tsx b/frontend/components/ui/aspect-ratio.tsx deleted file mode 100644 index d6a5226..0000000 --- a/frontend/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client" - -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" - -const AspectRatio = AspectRatioPrimitive.Root - -export { AspectRatio } diff --git a/frontend/components/ui/avatar.tsx b/frontend/components/ui/avatar.tsx deleted file mode 100644 index 51e507b..0000000 --- a/frontend/components/ui/avatar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client" - -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" - -import { cn } from "@/lib/utils" - -const Avatar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -Avatar.displayName = AvatarPrimitive.Root.displayName - -const AvatarImage = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName - -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName - -export { Avatar, AvatarImage, AvatarFallback } diff --git a/frontend/components/ui/breadcrumb.tsx b/frontend/components/ui/breadcrumb.tsx deleted file mode 100644 index 60e6c96..0000000 --- a/frontend/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode - } ->(({ ...props }, ref) =>