import React, { useState, useEffect, useCallback, useRef } from "react"; import { LOG_CONTAINER_HEIGHT } from "../constants"; import { Modal, ModalVariant, ModalHeader, ModalBody, ModalFooter, Button, Spinner, Alert, EmptyState, EmptyStateBody, Label, CodeBlock, CodeBlockCode, ExpandableSection, Content, ContentVariants, } from "@patternfly/react-core"; import { Table, Thead, Tr, Th, Tbody, Td, ThProps } from "@patternfly/react-table"; import { ArrowDownIcon, CheckCircleIcon } from "@patternfly/react-icons"; import { CachedVersion, listDowngrades, downgradePackage, formatSize, } from "../api"; import { sanitizeErrorMessage } from "../utils"; type ModalState = "loading" | "select" | "confirm" | "downgrading" | "success" | "error"; interface DowngradeModalProps { packageName: string; currentVersion: string; isOpen: boolean; onClose: () => void; } export const DowngradeModal: React.FC = ({ packageName, currentVersion, isOpen, onClose, }) => { const [state, setState] = useState("loading"); const [versions, setVersions] = useState([]); const [selectedVersion, setSelectedVersion] = useState(null); const [error, setError] = useState(null); const [log, setLog] = useState(""); const [isDetailsExpanded, setIsDetailsExpanded] = useState(false); const [activeSortIndex, setActiveSortIndex] = useState(null); const [activeSortDirection, setActiveSortDirection] = useState<"asc" | "desc">("desc"); const cancelRef = useRef<(() => void) | null>(null); const logContainerRef = useRef(null); const loadVersions = useCallback(async () => { if (!packageName) return; setState("loading"); setError(null); try { const response = await listDowngrades(packageName); const olderVersions = response.packages.filter((v) => v.is_older); setVersions(olderVersions); setState("select"); } catch (ex) { setState("error"); setError(ex instanceof Error ? ex.message : String(ex)); } }, [packageName]); useEffect(() => { if (isOpen) { loadVersions(); setSelectedVersion(null); setLog(""); } }, [isOpen, loadVersions]); useEffect(() => { return () => { if (cancelRef.current) { cancelRef.current(); } }; }, []); useEffect(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [log]); const handleSelectVersion = (version: CachedVersion) => { setSelectedVersion(version); setState("confirm"); }; const handleConfirmDowngrade = () => { if (!selectedVersion) return; setState("downgrading"); setLog(""); setIsDetailsExpanded(true); const { cancel } = downgradePackage( { onData: (data) => setLog((prev) => prev + data), onComplete: () => { setState("success"); cancelRef.current = null; }, onError: (err) => { setState("error"); setError(err); cancelRef.current = null; }, }, packageName, selectedVersion.version ); cancelRef.current = cancel; }; const handleCancel = () => { if (cancelRef.current) { cancelRef.current(); cancelRef.current = null; } setState("select"); setLog(""); }; const handleClose = () => { if (cancelRef.current) { cancelRef.current(); cancelRef.current = null; } onClose(); }; const getSortParams = (columnIndex: number): ThProps["sort"] | undefined => { if (columnIndex !== 0 && columnIndex !== 1) return undefined; return { sortBy: { index: activeSortIndex ?? undefined, direction: activeSortDirection, defaultDirection: "desc", }, onSort: (_event, index, direction) => { setActiveSortIndex(index); setActiveSortDirection(direction); }, columnIndex, }; }; const sortedVersions = React.useMemo(() => { return [...versions].sort((a, b) => { if (activeSortIndex === null) return 0; let comparison = 0; switch (activeSortIndex) { case 0: comparison = a.version.localeCompare(b.version); break; case 1: comparison = a.size - b.size; break; default: return 0; } return activeSortDirection === "asc" ? comparison : -comparison; }); }, [versions, activeSortIndex, activeSortDirection]); const renderContent = () => { switch (state) { case "loading": return ( Scanning package cache... ); case "error": return ( {sanitizeErrorMessage(error)} ); case "select": if (versions.length === 0) { return ( No older versions of {packageName} were found in the package cache. Only versions older than {currentVersion} can be used for downgrade. ); } return ( <> Select a version to downgrade {packageName} from{" "} {currentVersion}

Version | Size | Action
--- | --- | ---
{sortedVersions.map((v) => (
`{v.version}` | {formatSize(v.size)} | } onClick={() => handleSelectVersion(v)} > Downgrade
))}

); case "confirm": return ( Are you sure you want to downgrade {packageName}? {currentVersion} {" -> "} {selectedVersion?.version} Downgrading packages may cause dependency issues or break functionality. Only proceed if you know what you are doing. ); case "downgrading": return ( <>
Downgrading {packageName} to {selectedVersion?.version}...
setIsDetailsExpanded(expanded)} isExpanded={isDetailsExpanded} >
{log || "Starting..."}
); case "success": return ( {packageName} has been downgraded to {selectedVersion?.version}. ); default: return null; } }; const renderFooter = () => { switch (state) { case "select": return (
); case "confirm": return ( <>
); case "downgrading": return (
); case "success": return (
); case "error": return ( <>
); default: return null; } }; return ( {renderContent()} {renderFooter()} ); };