"use client"; import { ColumnDef, Row, RowSelectionState } from "@tanstack/react-table"; import { CornerDownRight, VolumeOff, VolumeX } from "lucide-react"; import { useContext, useState } from "react"; import { JiraDispatchActionItem } from "@/components/findings/jira-dispatch-action-item"; import { MuteFindingsModal } from "@/components/findings/mute-findings-modal"; import { Checkbox } from "@/components/shadcn"; import { ActionDropdown, ActionDropdownItem, } from "@/components/shadcn/dropdown"; import { DateWithTime } from "@/components/shadcn/entities"; import { EntityInfo } from "@/components/shadcn/entities/entity-info"; import { InfoField } from "@/components/shadcn/info-field/info-field"; import { Spinner } from "@/components/shadcn/spinner/spinner"; import { SeverityBadge } from "@/components/shadcn/table"; import { DataTableColumnHeader } from "@/components/shadcn/table/data-table-column-header"; import { getFailingForLabel } from "@/lib/date-utils"; import { buildJiraActionLabel } from "@/lib/jira-dispatch-action"; import { createJiraDispatchPayload } from "@/lib/jira-dispatch-selection"; import { buildFindingResourceContext } from "@/lib/lighthouse/context/contributions"; import { isCloud } from "@/lib/shared/env"; import { FindingResourceRow } from "@/types"; import type { FindingTriageContext, FindingTriageDetailLoadHandler, FindingTriageNoteLoadHandler, FindingTriageUpdateHandler, } from "@/types/findings-triage"; import { JIRA_DISPATCH_TARGET } from "@/types/integrations"; import { canMuteFindingResource } from "./finding-resource-selection"; import { FindingNoteActionItem, FindingTriageStatusCell, } from "./finding-triage-cells"; import { FindingsSelectionContext } from "./findings-selection-context"; import { LighthouseSkillsRowButton, LighthouseSkillsSubmenu, useLighthousePromptLaunch, useLighthouseSkillLaunch, } from "./lighthouse-skills-launch"; import { type DeltaType, NotificationIndicator, } from "./notification-indicator"; const buildFindingResourceTriageContext = ( resource: FindingResourceRow, findingTitle?: string, ): FindingTriageContext => ({ title: findingTitle || resource.checkId, resource: resource.resourceName, provider: resource.providerAlias, providerType: resource.providerType, }); // One finding-context item per resource row, shared by the leading Skills // pill and the ⋮ submenu so both launch with identical context. const buildResourceFindingItem = (resource: FindingResourceRow) => buildFindingResourceContext({ findingId: resource.findingId, checkId: resource.checkId, severity: resource.severity, status: resource.status, providerUid: resource.providerUid, resourceUid: resource.resourceUid, region: resource.region, }); const ResourceRowActions = ({ row, findingTitle, onSkillLaunchOpenDrawer, onTriageUpdateAction, onTriageNoteLoadAction, onTriageDetailLoadAction, }: { row: Row; findingTitle?: string; onSkillLaunchOpenDrawer?: (rowIndex: number) => void; onTriageUpdateAction?: FindingTriageUpdateHandler; onTriageNoteLoadAction?: FindingTriageNoteLoadHandler; onTriageDetailLoadAction?: FindingTriageDetailLoadHandler; }) => { const resource = row.original; const canMute = canMuteFindingResource(resource); const launchSkill = useLighthouseSkillLaunch(); const launchPrompt = useLighthousePromptLaunch(); const [isMuteModalOpen, setIsMuteModalOpen] = useState(false); const [resolvedIds, setResolvedIds] = useState([]); const [isResolving, setIsResolving] = useState(false); const { selectedFindingIds, clearSelection, resolveMuteIds, onMuteComplete } = useContext(FindingsSelectionContext) || { selectedFindingIds: [], clearSelection: () => {}, }; const isCurrentSelected = selectedFindingIds.includes(resource.findingId); const hasMultipleSelected = selectedFindingIds.length > 1; const getDisplayIds = (): string[] => { if (isCurrentSelected && hasMultipleSelected) { return selectedFindingIds; } return [resource.findingId]; }; const getMuteLabel = () => { if (resource.isMuted) return "Muted"; const ids = getDisplayIds(); if (ids.length > 1) return `Mute ${ids.length}`; return "Mute"; }; const displayIds = getDisplayIds(); const jiraPayload = createJiraDispatchPayload({ targetIds: displayIds, targetType: JIRA_DISPATCH_TARGET.FINDING_ID, findingTitle: findingTitle || resource.checkId, selectedResourceCount: displayIds.length, isFindingGroupSelection: true, }); const handleMuteClick = async () => { const displayIds = getDisplayIds(); // Single resource: findingId is already a real finding UUID if (displayIds.length === 1) { setResolvedIds(displayIds); setIsMuteModalOpen(true); return; } // Multi-select: resolve through context if (resolveMuteIds) { setIsResolving(true); const ids = await resolveMuteIds(displayIds); setResolvedIds(ids); setIsResolving(false); if (ids.length > 0) setIsMuteModalOpen(true); } else { setResolvedIds(displayIds); setIsMuteModalOpen(true); } }; const handleMuteComplete = () => { clearSelection(); setResolvedIds([]); onMuteComplete?.(); }; return ( <> {canMute && ( )}
e.stopPropagation()} > ) : isResolving ? ( ) : ( ) } label={isResolving ? "Resolving..." : getMuteLabel()} disabled={!canMute || isResolving} onSelect={handleMuteClick} /> {isCloud() && ( { onSkillLaunchOpenDrawer?.(row.index); launchSkill(skill, buildResourceFindingItem(resource)); }} onSubmitPrompt={(text) => { onSkillLaunchOpenDrawer?.(row.index); launchPrompt(text, buildResourceFindingItem(resource)); }} /> )}
); }; interface GetColumnFindingResourcesOptions { rowSelection: RowSelectionState; selectableRowCount: number; findingTitle?: string; // Skill launch (pill or ⋮ submenu) opens this row's finding drawer behind // the chat tab, so the run and the finding share the side panel. onSkillLaunchOpenDrawer?: (rowIndex: number) => void; onTriageUpdateAction?: FindingTriageUpdateHandler; onTriageNoteLoadAction?: FindingTriageNoteLoadHandler; onTriageDetailLoadAction?: FindingTriageDetailLoadHandler; } export function getColumnFindingResources({ rowSelection, selectableRowCount, findingTitle, onSkillLaunchOpenDrawer, onTriageUpdateAction, onTriageNoteLoadAction, onTriageDetailLoadAction, }: GetColumnFindingResourcesOptions): ColumnDef[] { const selectedCount = Object.values(rowSelection).filter(Boolean).length; const isAllSelected = selectedCount > 0 && selectedCount === selectableRowCount; const isSomeSelected = selectedCount > 0 && selectedCount < selectableRowCount; return [ // Combined column: notification + child icon + checkbox { id: "select", header: ({ table }) => { const headerChecked = isAllSelected ? true : isSomeSelected ? "indeterminate" : false; return (
{/* Mirrors the row's indicator + arrow so checkboxes stay aligned */}
table.toggleAllPageRowsSelected(checked === true) } onClick={(e) => e.stopPropagation()} aria-label="Select all resources" disabled={selectableRowCount === 0} />
); }, cell: ({ row }) => ( // relative: paints above the cell's hover-extension pseudo-element, // which would otherwise cover the in-flow checkbox and indicator.
{isCloud() ? ( onSkillLaunchOpenDrawer?.(row.index)} /> ) : ( )} row.toggleSelected(checked === true)} onClick={(e) => e.stopPropagation()} aria-label="Select resource" />
), enableSorting: false, enableHiding: false, }, // Affected failing resource — name + uid { id: "resource", header: ({ column }) => ( ), cell: ({ row }) => (
), enableSorting: false, }, // Provider — alias + uid (same style as Resource) { id: "provider", header: ({ column }) => ( ), cell: ({ row }) => (
), enableSorting: false, }, // Severity { id: "severity", header: ({ column }) => ( ), cell: ({ row }) => , enableSorting: false, }, // Service { id: "service", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.service || "-"} ), enableSorting: false, }, // Region { id: "region", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.region || "-"} ), enableSorting: false, }, // Last seen { id: "lastSeen", header: ({ column }) => ( ), cell: ({ row }) => ( ), enableSorting: false, }, // Failing for — duration since first_seen_at { id: "failingFor", header: ({ column }) => ( ), cell: ({ row }) => { const duration = getFailingForLabel(row.original.firstSeenAt); return ( {duration || "-"} ); }, enableSorting: false, }, // Triage — keep the compact label: these cells also render inside // expanded finding-group rows, which have no header row of their own. { id: "triage", header: ({ column }) => ( ), cell: ({ row }) => ( ), enableSorting: false, }, // Actions column — utility actions are kept last. { id: "actions", size: 56, header: () =>
, cell: ({ row }) => ( ), enableSorting: false, }, ]; }