"use client"; import { flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { AnimatePresence, motion } from "framer-motion"; import { ChevronsDown } from "lucide-react"; import { useImperativeHandle, useRef } from "react"; import { loadLatestFindingTriageNote, updateFindingTriage, } from "@/actions/findings"; import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; import { LoadingState } from "@/components/shadcn/spinner/loading-state"; import { TableCell, TableRow } from "@/components/ui/table"; import { useFindingGroupResourceState } from "@/hooks/use-finding-group-resource-state"; import { useScrollHint } from "@/hooks/use-scroll-hint"; import { cn } from "@/lib/utils"; import { FindingGroupRow } from "@/types"; import { getColumnFindingResources } from "./column-finding-resources"; import { FindingsSelectionContext } from "./findings-selection-context"; import { getFilteredFindingGroupResourceCount, getFindingGroupEmptyStateMessage, getFindingGroupSkeletonCount, } from "./inline-resource-container.utils"; import { ResourceDetailDrawer } from "./resource-detail-drawer"; export interface InlineResourceContainerHandle { /** Soft-refresh resources (re-fetch page 1 without skeletons). */ refresh: () => void; /** Clear internal row selection and notify parent. */ clearSelection: () => void; } interface InlineResourceContainerProps { group: FindingGroupRow; resolvedFilters: Record; hasHistoricalData: boolean; resourceSearch: string; columnCount: number; /** Called with selected finding IDs (real UUIDs) for parent-level mute */ onResourceSelectionChange: (findingIds: string[]) => void; ref?: React.Ref; } // NOTE: We intentionally do NOT auto-select child resources when a parent group // is selected. Group-level mute resolution now fetches the group's visible // resources separately. Auto-selecting children would still require syncing state // with infinite scroll (resources load 10 at a time), causing cascading setState // during render and confusing partial selections. Resource-level checkboxes are // for selecting a specific subset independently. /** Max skeleton rows that fit in the 440px scroll container */ const MAX_SKELETON_ROWS = 7; const ACTIONS_COLUMN_ID = "actions"; const COMPACT_LABELED_COLUMN_IDS = new Set([ "service", "region", "lastSeen", "failingFor", "triage", ]); const STICKY_RESOURCE_ACTION_CELL_CLASS = "sticky right-0 z-20 min-w-12 last:rounded-r-none! overflow-visible bg-bg-neutral-secondary before:pointer-events-none before:absolute before:inset-y-0 before:-left-8 before:w-8 before:bg-gradient-to-r before:from-transparent before:to-bg-neutral-secondary before:content-[''] group-hover:bg-bg-neutral-tertiary group-hover:before:to-bg-neutral-tertiary group-data-[state=selected]:bg-bg-neutral-tertiary group-data-[state=selected]:before:to-bg-neutral-tertiary"; const getResourceCellClassName = (columnId: string) => cn( COMPACT_LABELED_COLUMN_IDS.has(columnId) && "align-top", columnId === ACTIONS_COLUMN_ID && STICKY_RESOURCE_ACTION_CELL_CLASS, ); function ResourceSkeletonRow({ isEmptyStateSized = false, }: { isEmptyStateSized?: boolean; }) { const cellClassName = isEmptyStateSized ? "h-24 py-3" : "py-3"; return ( {/* Select: indicator + corner arrow + checkbox */}
{/* Status */} {/* Resource: name + uid */}
{/* Provider: alias + uid */}
{/* Severity */}
{/* Service */} {/* Region */} {/* Last seen */} {/* Failing for */} {/* Triage */} {/* Actions */}
); } export function InlineResourceContainer({ group, resolvedFilters, hasHistoricalData, resourceSearch, columnCount, onResourceSelectionChange, ref, }: InlineResourceContainerProps) { const scrollContainerRef = useRef(null); const filters: Record = { ...resolvedFilters }; if (resourceSearch) { filters["filter[name__icontains]"] = resourceSearch; } const skeletonRowCount = getFindingGroupSkeletonCount( group, filters, MAX_SKELETON_ROWS, ); const filteredResourceCount = getFilteredFindingGroupResourceCount( group, filters, ); const { rowSelection, resources, isLoading, sentinelRef, refresh, drawer, handleDrawerMuteComplete, selectedFindingIds, selectableRowCount, getRowCanSelect, clearSelection, isSelected, handleMuteComplete, handleRowSelectionChange, resolveSelectedFindingIds, updateTriageOptimistically, } = useFindingGroupResourceState({ group, filters, hasHistoricalData, onResourceSelectionChange, scrollContainerRef, }); // Scroll hint: shows "scroll for more" when content overflows const { containerRef: scrollHintContainerRef, sentinelRef: scrollHintSentinelRef, showScrollHint, } = useScrollHint({ refreshToken: resources.length }); // Combine scrollContainerRef (for IntersectionObserver root) with scrollHintContainerRef const combinedScrollRef = (node: HTMLDivElement | null) => { scrollContainerRef.current = node; scrollHintContainerRef(node); }; useImperativeHandle(ref, () => ({ refresh, clearSelection })); const columns = getColumnFindingResources({ rowSelection, selectableRowCount, findingTitle: group.checkTitle, onTriageUpdateAction: (input) => updateTriageOptimistically(input, updateFindingTriage), onTriageNoteLoadAction: loadLatestFindingTriageNote, }); const table = useReactTable({ data: resources, columns, enableRowSelection: getRowCanSelect, getCoreRowModel: getCoreRowModel(), onRowSelectionChange: handleRowSelectionChange, manualPagination: true, state: { rowSelection, }, }); const rows = table.getRowModel().rows; return (
{/* Resource rows or skeleton placeholder */} {isLoading && rows.length === 0 ? ( Array.from({ length: skeletonRowCount }).map((_, i) => ( )) ) : rows.length > 0 ? ( rows.map((row) => ( { // Don't open drawer if clicking interactive elements // (links, buttons, checkboxes, dropdown items) const target = e.target as HTMLElement; if ( target.closest( "a, button, input, [role=menuitem]", ) ) return; drawer.openDrawer(row.index); }} > {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))} )) ) : ( {getFindingGroupEmptyStateMessage(group, filters)} )}
{/* Loading state for infinite scroll (subsequent pages only) */} {isLoading && rows.length > 0 && ( )} {/* Sentinel for scroll hint detection */}
{/* Sentinel for infinite scroll */}
{/* Gradients rendered after scroll container so they paint on top */}
{/* Scroll hint */} {showScrollHint && (
Scroll for more
)}
{ if (!open) drawer.closeDrawer(); }} isLoading={drawer.isLoading} isNavigating={drawer.isNavigating} checkMeta={drawer.checkMeta} currentIndex={drawer.currentIndex} totalResources={drawer.totalResources} currentResource={drawer.currentResource} currentFinding={drawer.currentFinding} otherFindings={drawer.otherFindings} showSyntheticResourceHint={group.resourcesTotal === 0} onNavigatePrev={drawer.navigatePrev} onNavigateNext={drawer.navigateNext} onMuteComplete={handleDrawerMuteComplete} onTriageUpdate={drawer.patchTriageUpdate} /> ); }