/******************************************************************************** * 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, useState } from 'react'; import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox, Typography, Chip, } from '@mui/material'; import { FileDecision } from '../../../context/scan-admin'; import { ConditionalTooltip, formatDateTime } from './index'; import { useTheme } from '@mui/material/styles'; interface FileTableProps { files: FileDecision[]; type: 'allowed' | 'blocked'; selectedFiles?: Set; onSelectionChange?: (fileIds: Set) => void; } export const FileTable: FunctionComponent = (props) => { const { files, type, selectedFiles = new Set(), onSelectionChange } = props; const [internalSelection, setInternalSelection] = useState>(selectedFiles); const theme = useTheme(); const [columnWidths, setColumnWidths] = useState({ file: 250, type: 100, date: 200, actionBy: 130, extension: 250, publisher: 150, version: 100, }); const selection = onSelectionChange ? selectedFiles : internalSelection; const setSelection = onSelectionChange || setInternalSelection; const handleSelectAll = (event: React.ChangeEvent) => { if (event.target.checked) { const allIds = new Set(files.map(f => f.id)); setSelection(allIds); } else { setSelection(new Set()); } }; const handleSelectOne = (fileId: string) => { const newSelection = new Set(selection); if (newSelection.has(fileId)) { newSelection.delete(fileId); } else { newSelection.add(fileId); } setSelection(newSelection); }; const isSelected = (fileId: string) => selection.has(fileId); const allSelected = files.length > 0 && selection.size === files.length; const someSelected = selection.size > 0 && selection.size < files.length; const handleDoubleClick = (column: keyof typeof columnWidths, event: React.MouseEvent) => { event.preventDefault(); const minWidth = 80; const columns: (keyof typeof columnWidths)[] = ['file', 'type', 'date', 'actionBy', 'extension', 'publisher', 'version']; const currentIndex = columns.indexOf(column); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); if (!context) return; context.font = '14px Roboto, sans-serif'; let idealWidth = minWidth; const padding = 32; const headerTexts: Record = { file: 'File', type: 'Type', date: type === 'allowed' ? 'Date Allowed' : 'Date Blocked', actionBy: type === 'allowed' ? 'Allowed By' : 'Blocked By', extension: 'Extension', publisher: 'Publisher', version: 'Version', }; idealWidth = Math.max(idealWidth, context.measureText(headerTexts[column]).width + padding); files.forEach(file => { let text = ''; switch (column) { case 'file': { const fileNameWidth = context.measureText(file.fileName).width; const fileHashWidth = context.measureText(file.fileHash).width; idealWidth = Math.max(idealWidth, fileNameWidth + padding, fileHashWidth + padding); return; } case 'type': text = file.fileType; break; case 'date': { text = formatDateTime(file.dateDecided); break; } case 'actionBy': text = file.decidedBy; break; case 'extension': { const displayNameWidth = context.measureText(file.displayName).width; const namespaceWidth = context.measureText(`${file.namespace}.${file.extensionName}`).width; idealWidth = Math.max(idealWidth, displayNameWidth + padding, namespaceWidth + padding); return; } case 'publisher': text = file.publisher; break; case 'version': text = file.version; break; } if (text) { const width = context.measureText(text).width + padding; idealWidth = Math.max(idealWidth, width); } }); idealWidth = Math.min(idealWidth, 600); const currentWidth = columnWidths[column]; const growthNeeded = idealWidth - currentWidth; if (growthNeeded <= 0) { setColumnWidths(prev => ({ ...prev, [column]: idealWidth, })); return; } let availableSpace = 0; for (let i = currentIndex + 1; i < columns.length; i++) { const col = columns[i]; availableSpace += columnWidths[col] - minWidth; } if (availableSpace <= 0) { return; } const actualGrowth = Math.min(growthNeeded, availableSpace); const newWidth = currentWidth + actualGrowth; const newWidths = { ...columnWidths }; newWidths[column] = newWidth; let remainingShrink = actualGrowth; for (let i = currentIndex + 1; i < columns.length && remainingShrink > 0; i++) { const col = columns[i]; const availableShrink = columnWidths[col] - minWidth; const shrinkAmount = Math.min(remainingShrink, availableShrink); newWidths[col] = columnWidths[col] - shrinkAmount; remainingShrink -= shrinkAmount; } setColumnWidths(newWidths); }; const handleMouseDown = (column: keyof typeof columnWidths, event: React.MouseEvent) => { event.preventDefault(); const startX = event.clientX; const startWidths = { ...columnWidths }; const minWidth = 80; const columns: (keyof typeof columnWidths)[] = ['file', 'type', 'date', 'actionBy', 'extension', 'publisher', 'version']; const currentIndex = columns.indexOf(column); if (currentIndex === columns.length - 1) return; const handleMouseMove = (e: MouseEvent) => { const diff = e.clientX - startX; const newWidths = { ...startWidths }; if (diff > 0) { let remainingDiff = diff; const currentGrowth = Math.min(remainingDiff, Number.MAX_SAFE_INTEGER); newWidths[column] = startWidths[column] + currentGrowth; for (let i = currentIndex + 1; i < columns.length && remainingDiff > 0; i++) { const col = columns[i]; const availableShrink = startWidths[col] - minWidth; const shrinkAmount = Math.min(remainingDiff, availableShrink); newWidths[col] = startWidths[col] - shrinkAmount; remainingDiff -= shrinkAmount; } newWidths[column] = startWidths[column] + (diff - remainingDiff); } else if (diff < 0) { let remainingDiff = Math.abs(diff); const currentShrink = Math.min(remainingDiff, startWidths[column] - minWidth); newWidths[column] = startWidths[column] - currentShrink; remainingDiff -= currentShrink; if (remainingDiff > 0) { for (let i = currentIndex - 1; i >= 0 && remainingDiff > 0; i--) { const col = columns[i]; const availableShrink = startWidths[col] - minWidth; const shrinkAmount = Math.min(remainingDiff, availableShrink); newWidths[col] = startWidths[col] - shrinkAmount; remainingDiff -= shrinkAmount; } } const totalShrunk = Math.abs(diff) - remainingDiff; newWidths[columns[currentIndex + 1]] = startWidths[columns[currentIndex + 1]] + totalShrunk; } setColumnWidths(newWidths); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; const cellStyle = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' as const, }; const truncateStyle = { display: 'block', ...cellStyle, }; return ( File
handleMouseDown('file', e)} onDoubleClick={(e) => handleDoubleClick('file', e)} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '5px', cursor: 'col-resize', userSelect: 'none', }} /> Type
handleMouseDown('type', e)} onDoubleClick={(e) => handleDoubleClick('type', e)} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '5px', cursor: 'col-resize', userSelect: 'none', }} /> {type === 'allowed' ? 'Date Allowed' : 'Date Blocked'}
handleMouseDown('date', e)} onDoubleClick={(e) => handleDoubleClick('date', e)} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '5px', cursor: 'col-resize', userSelect: 'none', }} /> {type === 'allowed' ? 'Allowed By' : 'Blocked By'}
handleMouseDown('actionBy', e)} onDoubleClick={(e) => handleDoubleClick('actionBy', e)} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '5px', cursor: 'col-resize', userSelect: 'none', }} /> Extension
handleMouseDown('extension', e)} onDoubleClick={(e) => handleDoubleClick('extension', e)} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '5px', cursor: 'col-resize', userSelect: 'none', }} /> Publisher
handleMouseDown('publisher', e)} onDoubleClick={(e) => handleDoubleClick('publisher', e)} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '5px', cursor: 'col-resize', userSelect: 'none', }} /> Version
handleMouseDown('version', e)} onDoubleClick={(e) => handleDoubleClick('version', e)} style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '5px', cursor: 'col-resize', userSelect: 'none', }} /> {files.length === 0 ? ( No {type} files ) : ( files.map((file) => { const selected = isSelected(file.id); const dateField = file.dateDecided; const actionByField = file.decidedBy; return ( handleSelectOne(file.id)} selected={selected} sx={{ cursor: 'pointer', '&.Mui-selected': { backgroundColor: theme.palette.selected.background, }, '&.Mui-selected:hover': { backgroundColor: theme.palette.selected.backgroundHover, }, }} > {file.fileName} {file.fileHash} {formatDateTime(dateField)} {actionByField} {file.displayName} {file.namespace}.{file.extensionName} {file.publisher} {file.version} ); }) )}
); };