/******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ import { FunctionComponent, PropsWithChildren, useState } from 'react'; import { Box, Typography, Link, IconButton, Tooltip } from '@mui/material'; import { styled, useTheme } from '@mui/material/styles'; import { Check as CheckIcon, Warning as WarningAmberIcon, } from '@mui/icons-material'; import { ScanResult } from '../../../context/scan-admin'; import { ConditionalTooltip, formatDateTime, formatDuration } from '../common'; import { isRunning, hasDownload, getFileName } from './utils'; /** * Grid cell positioned by row/column within the parent CSS Grid. * Note: MUI's Grid/Grid2 components are flexbox-based, not CSS Grid. */ const GridCell = styled(Box, { shouldForwardProp: (prop) => prop !== 'row' && prop !== 'column' && prop !== 'columnSpan', })<{ row: number; column: number; columnSpan?: number }>(({ row, column, columnSpan }) => ({ gridRow: String(row), gridColumn: columnSpan ? `${column} / span ${columnSpan}` : String(column), minWidth: 0, })); /** Typography with text-overflow ellipsis */ const EllipsisText = styled(Typography)({ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }); /** Box with text-overflow ellipsis */ const EllipsisBox = styled(Box)({ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }); /** Link with text-overflow ellipsis */ const EllipsisLink = styled(Link)({ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }); /** Shimmer-animated text for in-progress scan states */ const ShimmerText = styled(EllipsisText)(({ theme }) => ({ background: theme.palette.gray.gradient, backgroundSize: '200% 100%', backgroundClip: 'text', WebkitBackgroundClip: 'text', color: 'transparent', animation: 'shimmer 2s infinite', '@keyframes shimmer': { '0%': { backgroundPosition: '200% 0' }, '100%': { backgroundPosition: '-200% 0' }, }, })); /** Circular checkbox outline with checked/hover state transitions */ const CheckboxOutline = styled(Box, { shouldForwardProp: (prop) => prop !== 'isChecked' && prop !== 'isHovering', })<{ isChecked: boolean; isHovering: boolean }>(({ theme, isChecked, isHovering }) => { const borderColor = isHovering ? theme.palette.selected.border : theme.palette.scanBackground.light; const uncheckedBg = isHovering ? theme.palette.selected.background : 'transparent'; return { position: 'absolute', width: 36, height: 36, borderRadius: '50%', border: isChecked ? 'none' : `2px solid ${borderColor}`, backgroundColor: isChecked ? theme.palette.secondary.main : uncheckedBg, transition: 'border-color 0.2s, background-color 0.2s', }; }); /** Section caption label (e.g. "Publisher", "Version") */ const CaptionLabel: FunctionComponent = ({ children }) => ( {children} ); /** N/A placeholder for unavailable data */ const NotAvailable: FunctionComponent = () => ( N/A ); /** Publisher name with external link */ const PublisherCell: FunctionComponent<{ publisher: string; publisherUrl: string | null }> = ({ publisher, publisherUrl, }) => ( <> Publisher {publisher} ); /** Extension version display */ const VersionCell: FunctionComponent<{ version: string }> = ({ version }) => ( <> Version {version} ); /** Target platform display */ const PlatformCell: FunctionComponent<{ targetPlatform: string }> = ({ targetPlatform }) => ( <> Platform {targetPlatform} ); /** Download link with optional quarantine warning icon */ const DownloadCell: FunctionComponent<{ scan: ScanResult }> = ({ scan }) => { const theme = useTheme(); if (isRunning(scan.status)) { return ( <> Download ); } if (hasDownload(scan) && scan.downloadUrl) { return ( <> Download {scan.status === 'QUARANTINED' && ( )} {getFileName(scan.downloadUrl)} ); } return ( <> Download ); }; /** Circular selection checkbox with hover/check animations */ const SelectionCheckbox: FunctionComponent<{ checked?: boolean; onChange?: (checked: boolean) => void; }> = ({ checked = false, onChange }) => { const theme = useTheme(); const [isHovering, setIsHovering] = useState(false); const uncheckedIconColor = isHovering ? theme.palette.selected.border : theme.palette.scanBackground.light; return ( onChange?.(!checked)} onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} disableRipple sx={{ padding: 0, width: 36, height: 36, backgroundColor: 'transparent' }} > ); }; /** Scan start timestamp */ const ScanStartCell: FunctionComponent<{ dateScanStarted: string }> = ({ dateScanStarted }) => ( <> Scan Start {formatDateTime(dateScanStarted)} ); /** Scan end timestamp with shimmer animation for running scans */ const ScanEndCell: FunctionComponent<{ status: ScanResult['status']; dateScanEnded: string | null }> = ({ status, dateScanEnded, }) => { const renderValue = () => { if (isRunning(status)) { return ( {status}... ); } if (dateScanEnded) { return ( {formatDateTime(dateScanEnded)} ); } return ; }; return ( <> Scan End {renderValue()} ); }; /** Scan duration with live counter for running scans */ const ScanDurationCell: FunctionComponent<{ status: ScanResult['status']; dateScanStarted: string; dateScanEnded: string | null; liveDuration: string; }> = ({ status, dateScanStarted, dateScanEnded, liveDuration }) => ( <> Scan Duration {isRunning(status) ? ( {liveDuration} ) : ( {formatDuration(dateScanStarted, dateScanEnded || undefined)} )} ); /** Admin decision status for quarantined extensions */ const DecisionStatusCell: FunctionComponent<{ adminDecision: ScanResult['adminDecision'] }> = ({ adminDecision }) => { const theme = useTheme(); if (adminDecision) { const isAllowed = adminDecision.decision.toLowerCase() === 'allowed'; return ( {isAllowed ? 'ALLOWED' : 'BLOCKED'} ); } return ( NEEDS REVIEW ); }; interface ScanCardContentProps { scan: ScanResult; showCheckbox?: boolean; checked?: boolean; onCheckboxChange?: (id: string, checked: boolean) => void; liveDuration: string; } /** * Content section of the ScanCard containing: * - Publisher, Version, Platform, Download (Row 2) * - Scan Start, Scan End, Duration, Decision Status (Row 3) * - Checkbox for selection */ export const ScanCardContent: FunctionComponent = ({ scan, showCheckbox, checked, onCheckboxChange, liveDuration, }) => ( <> {/* ROW 2 - Publisher, Version, Platform, Download, Checkbox */} {showCheckbox && ( onCheckboxChange?.(scan.id, newChecked)} /> )} {/* ROW 3 - Scan Start, Scan End, Duration, Decision Status */} {scan.status === 'QUARANTINED' && ( )} );