mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? We've quite a few instances where some buttons have a dropdown appendage. The focus state for these were broken as well as visually regarding the separator. This first pass fixes the instances we have in studio, I've left potentially adding this to our design system fragment components as another PR. | Before | After | |--------|--------| | <img width="531" height="133" alt="Screenshot 2026-08-13 at 11 29 36" src="https://github.com/user-attachments/assets/70747fd0-11d4-4670-85fa-d76f3564837b" /> | <img width="519" height="130" alt="Screenshot 2026-08-13 at 11 40 15" src="https://github.com/user-attachments/assets/5920bebb-81ce-4962-908b-5f61526ca7ca" /> | | <img width="538" height="146" alt="Screenshot 2026-08-13 at 11 29 48" src="https://github.com/user-attachments/assets/f1c7018f-cd43-47fa-b4c0-045af350f80b" /> | <img width="515" height="148" alt="Screenshot 2026-08-13 at 11 39 56" src="https://github.com/user-attachments/assets/fee09113-433a-4b5d-ac68-3ab3f2565d34" /> | | <img width="538" height="143" alt="Screenshot 2026-08-13 at 11 29 52" src="https://github.com/user-attachments/assets/3063d391-99b2-4599-9cdf-edd0b2cdfdf0" /> | <img width="529" height="159" alt="Screenshot 2026-08-13 at 11 40 05" src="https://github.com/user-attachments/assets/4bdeb9e0-8324-45df-a211-8c7fd0ee11a6" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved keyboard focus visibility across Studio controls, including token management, email settings, replication, log drains, query insights, infrastructure, storage, and assistant actions. * Focused buttons in adjacent or split-button groups now appear above neighboring controls, preventing borders and overlays from obscuring the active selection. * Preserved existing button behavior, layout, and appearance while improving focus-state clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
596 lines
22 KiB
TypeScript
596 lines
22 KiB
TypeScript
import { ArrowDown, ArrowRight, ArrowUp, ChevronDown, ExternalLink, ScanSearch } from 'lucide-react'
|
||
import { useMemo, type RefObject } from 'react'
|
||
import { type Column, type DataGridHandle } from 'react-data-grid'
|
||
import {
|
||
Button,
|
||
cn,
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
Tooltip,
|
||
TooltipContent,
|
||
TooltipTrigger,
|
||
} from 'ui'
|
||
import { CodeBlock } from 'ui-patterns/CodeBlock'
|
||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||
|
||
import { buildQueryInsightFixPrompt } from '../../QueryPerformance/QueryPerformance.ai'
|
||
import { QUERY_PERFORMANCE_ROLE_DESCRIPTION } from '../../QueryPerformance/QueryPerformance.constants'
|
||
import type { ClassifiedQuery } from '../QueryInsightsHealth/QueryInsightsHealth.types'
|
||
import {
|
||
ISSUE_DOT_COLORS,
|
||
ISSUE_ICONS,
|
||
NON_SORTABLE_COLUMNS,
|
||
QUERY_INSIGHTS_EXPLORER_COLUMNS,
|
||
} from '../QueryInsightsTable/QueryInsightsTable.constants'
|
||
import {
|
||
formatDuration,
|
||
getColumnName,
|
||
getTableName,
|
||
} from '../QueryInsightsTable/QueryInsightsTable.utils'
|
||
import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
|
||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||
|
||
interface UseQueryInsightsTableColumnsParams {
|
||
sort: { column: string; order: 'asc' | 'desc' }
|
||
setSort: (config: { column: string; order: 'asc' | 'desc' } | null) => void
|
||
timeConsumedWidth: number
|
||
triageQueryColWidth: number
|
||
gridRef: RefObject<DataGridHandle | null>
|
||
setSelectedRow: (idx: number) => void
|
||
setSelectedTriageRow: (idx: number | undefined) => void
|
||
setSheetView: (view: 'details' | 'indexes' | 'explain') => void
|
||
handleGoToLogs: () => void
|
||
handleAiSuggestedFix: (item: ClassifiedQuery) => void
|
||
}
|
||
|
||
export function useQueryInsightsTableColumns({
|
||
sort,
|
||
setSort,
|
||
timeConsumedWidth,
|
||
triageQueryColWidth,
|
||
gridRef,
|
||
setSelectedRow,
|
||
setSelectedTriageRow,
|
||
setSheetView,
|
||
handleGoToLogs,
|
||
handleAiSuggestedFix,
|
||
}: UseQueryInsightsTableColumnsParams): {
|
||
columns: Column<ClassifiedQuery>[]
|
||
triageColumns: Column<ClassifiedQuery>[]
|
||
} {
|
||
const columns = useMemo(() => {
|
||
return QUERY_INSIGHTS_EXPLORER_COLUMNS.map((col) => {
|
||
const isSortable = !NON_SORTABLE_COLUMNS.includes(col.id as never)
|
||
|
||
const result: Column<ClassifiedQuery> = {
|
||
key: col.id,
|
||
name: col.name,
|
||
cellClass: `column-${col.id}`,
|
||
resizable: true,
|
||
minWidth: col.id === 'prop_total_time' ? timeConsumedWidth : (col.minWidth ?? 120),
|
||
sortable: isSortable,
|
||
headerCellClass: 'first:pl-6 cursor-pointer',
|
||
renderHeaderCell: () => {
|
||
return (
|
||
<div className="flex items-center justify-between text-xs w-full">
|
||
<div className="flex items-center gap-x-2">
|
||
<p className="text-foreground! font-medium">{col.name}</p>
|
||
{col.description && (
|
||
<p className="text-foreground-lighter font-normal">{col.description}</p>
|
||
)}
|
||
</div>
|
||
|
||
{isSortable && (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="text"
|
||
size="tiny"
|
||
className="p-1 h-5 w-5 shrink-0"
|
||
icon={<ChevronDown size={14} className="text-foreground-muted" />}
|
||
onClick={(e) => e.stopPropagation()}
|
||
/>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-48">
|
||
<DropdownMenuItem
|
||
onClick={() => setSort({ column: col.id, order: 'asc' })}
|
||
className={cn(
|
||
'flex gap-2',
|
||
sort?.column === col.id && sort?.order === 'asc' && 'text-foreground'
|
||
)}
|
||
>
|
||
<ArrowUp size={14} />
|
||
Sort Ascending
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem
|
||
onClick={() => setSort({ column: col.id, order: 'desc' })}
|
||
className={cn(
|
||
'flex gap-2',
|
||
sort?.column === col.id && sort?.order === 'desc' && 'text-foreground'
|
||
)}
|
||
>
|
||
<ArrowDown size={14} />
|
||
Sort Descending
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
renderCell: (props) => {
|
||
const row = props.row
|
||
const value = row[col.id]
|
||
|
||
if (col.id === 'query') {
|
||
const IssueIcon = row.issueType ? ISSUE_ICONS[row.issueType] : null
|
||
return (
|
||
<div className="w-full flex items-center gap-x-3 group">
|
||
<div className="shrink-0 w-6">
|
||
{row.issueType && IssueIcon && (
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<div
|
||
className={cn(
|
||
'h-6 w-6 rounded-full border flex items-center justify-center cursor-default',
|
||
ISSUE_DOT_COLORS[row.issueType]?.border,
|
||
ISSUE_DOT_COLORS[row.issueType]?.background
|
||
)}
|
||
>
|
||
<IssueIcon size={14} className={ISSUE_DOT_COLORS[row.issueType].color} />
|
||
</div>
|
||
</TooltipTrigger>
|
||
{row.hint && (
|
||
<TooltipContent side="top" className="max-w-[260px]">
|
||
{row.hint}
|
||
</TooltipContent>
|
||
)}
|
||
</Tooltip>
|
||
)}
|
||
</div>
|
||
<CodeBlock
|
||
language="pgsql"
|
||
className="bg-transparent! p-0! m-0! border-none! whitespace-nowrap! [&>code]:whitespace-nowrap! [&>code]:wrap-break-word overflow-visible! truncate! w-full! pr-20! pointer-events-none"
|
||
wrapperClassName="max-w-full! flex-1"
|
||
hideLineNumbers
|
||
hideCopy
|
||
value={typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : ''}
|
||
wrapLines={false}
|
||
/>
|
||
<ButtonTooltip
|
||
tooltip={{ content: { text: 'Query details' } }}
|
||
icon={<ArrowRight size={14} />}
|
||
size="tiny"
|
||
variant="default"
|
||
onClick={(e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
setSelectedRow(props.rowIdx)
|
||
setSheetView('details')
|
||
gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx })
|
||
}}
|
||
className="p-1 shrink-0 -translate-x-2 group-hover:flex hidden"
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (col.id === 'prop_total_time') {
|
||
const percentage = row.prop_total_time || 0
|
||
const totalTime = row.total_time || 0
|
||
const fillWidth = Math.min(percentage, 100)
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
<div
|
||
className="absolute inset-0 bg-foreground transition-all duration-200 z-0"
|
||
style={{ width: `${fillWidth}%`, opacity: 0.04 }}
|
||
/>
|
||
{percentage && totalTime ? (
|
||
<span className="flex items-center justify-end gap-x-1.5">
|
||
<span
|
||
className={cn(percentage.toFixed(1) === '0.0' && 'text-foreground-lighter')}
|
||
>
|
||
{percentage.toFixed(1)}%
|
||
</span>
|
||
<span className="text-muted">/</span>
|
||
<span
|
||
className={cn(
|
||
formatDuration(totalTime) === '0ms' && 'text-foreground-lighter'
|
||
)}
|
||
>
|
||
{formatDuration(totalTime)}
|
||
</span>
|
||
</span>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (col.id === 'calls') {
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
||
{value.toLocaleString()}
|
||
</p>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (col.id === 'max_time' || col.id === 'mean_time' || col.id === 'min_time') {
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||
<p className={cn(value.toFixed(0) === '0' && 'text-foreground-lighter')}>
|
||
{Math.round(value).toLocaleString()}ms
|
||
</p>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (col.id === 'rows_read') {
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
||
{value.toLocaleString()}
|
||
</p>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (col.id === 'cache_hit_rate') {
|
||
const num = typeof value === 'number' ? value : parseFloat(value ?? '0')
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
{typeof num === 'number' && !isNaN(num) && isFinite(num) ? (
|
||
<p className={cn(num.toFixed(2) === '0.00' && 'text-foreground-lighter')}>
|
||
{num.toLocaleString(undefined, {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
})}
|
||
%
|
||
</p>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (col.id === 'rolname') {
|
||
return (
|
||
<div className="w-full flex flex-col justify-center">
|
||
{value ? (
|
||
<span className="flex items-center gap-x-1">
|
||
<p className="font-mono text-xs">{value}</p>
|
||
<InfoTooltip align="end" alignOffset={-12} className="w-56">
|
||
{
|
||
QUERY_PERFORMANCE_ROLE_DESCRIPTION.find((r) => r.name === value)
|
||
?.description
|
||
}
|
||
</InfoTooltip>
|
||
</span>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (col.id === 'application_name') {
|
||
return (
|
||
<div className="w-full flex flex-col justify-center">
|
||
{value ? (
|
||
<p className="font-mono text-xs">{value}</p>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return null
|
||
},
|
||
}
|
||
return result
|
||
})
|
||
}, [sort, setSort, timeConsumedWidth, gridRef, setSelectedRow, setSheetView])
|
||
|
||
const triageColumns = useMemo(
|
||
(): Column<ClassifiedQuery>[] => [
|
||
{
|
||
key: 'query',
|
||
name: 'Query',
|
||
minWidth: triageQueryColWidth,
|
||
width: triageQueryColWidth,
|
||
resizable: true,
|
||
headerCellClass: 'first:pl-6 cursor-default',
|
||
renderHeaderCell: () => (
|
||
<div className="flex items-center text-xs w-full">
|
||
<p className="text-foreground! font-medium">Query</p>
|
||
</div>
|
||
),
|
||
renderCell: (props) => {
|
||
const row = props.row as ClassifiedQuery
|
||
const IssueIcon = row.issueType ? ISSUE_ICONS[row.issueType] : null
|
||
return (
|
||
<div className="w-full flex items-center gap-x-3 group">
|
||
<div className="shrink-0 w-6">
|
||
{row.issueType && IssueIcon && (
|
||
<div
|
||
className={cn(
|
||
'h-6 w-6 rounded-full border flex items-center justify-center',
|
||
ISSUE_DOT_COLORS[row.issueType]?.border,
|
||
ISSUE_DOT_COLORS[row.issueType]?.background
|
||
)}
|
||
>
|
||
<IssueIcon size={14} className={ISSUE_DOT_COLORS[row.issueType].color} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-xs font-mono text-foreground truncate">
|
||
{row.queryType ?? '–'}
|
||
{getTableName(row.query) && (
|
||
<>
|
||
{' '}
|
||
<span className="text-foreground-lighter">in</span> {getTableName(row.query)}
|
||
</>
|
||
)}
|
||
{getColumnName(row.query) && (
|
||
<>
|
||
<span className="text-foreground-lighter">,</span> {getColumnName(row.query)}
|
||
</>
|
||
)}
|
||
</p>
|
||
<p
|
||
className={cn(
|
||
'text-xs mt-0.5 font-mono truncate',
|
||
row.issueType === 'error' && 'text-destructive-600',
|
||
row.issueType === 'index' && 'text-warning-600',
|
||
row.issueType === 'slow' && 'text-foreground-lighter'
|
||
)}
|
||
>
|
||
{row.hint}
|
||
</p>
|
||
</div>
|
||
<ButtonTooltip
|
||
tooltip={{ content: { text: 'Query details' } }}
|
||
icon={<ArrowRight size={14} />}
|
||
size="tiny"
|
||
variant="default"
|
||
onClick={(e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
setSelectedTriageRow(props.rowIdx)
|
||
setSheetView('details')
|
||
}}
|
||
className="p-1 shrink-0 group-hover:flex hidden"
|
||
/>
|
||
</div>
|
||
)
|
||
},
|
||
},
|
||
{
|
||
key: 'prop_total_time',
|
||
name: 'Time consumed',
|
||
minWidth: timeConsumedWidth,
|
||
resizable: true,
|
||
cellClass: 'column-prop_total_time',
|
||
headerCellClass: 'cursor-default',
|
||
renderHeaderCell: () => (
|
||
<div className="flex items-center text-xs w-full">
|
||
<p className="text-foreground! font-medium">Time consumed</p>
|
||
</div>
|
||
),
|
||
renderCell: (props) => {
|
||
const row = props.row as ClassifiedQuery
|
||
const percentage = row.prop_total_time || 0
|
||
const totalTime = row.total_time || 0
|
||
const fillWidth = Math.min(percentage, 100)
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
<div
|
||
className="absolute inset-0 bg-foreground transition-all duration-200 z-0"
|
||
style={{ width: `${fillWidth}%`, opacity: 0.04 }}
|
||
/>
|
||
{percentage && totalTime ? (
|
||
<span className="flex items-center justify-end gap-x-1.5">
|
||
<span
|
||
className={cn(percentage.toFixed(1) === '0.0' && 'text-foreground-lighter')}
|
||
>
|
||
{percentage.toFixed(1)}%
|
||
</span>
|
||
<span className="text-muted">/</span>
|
||
<span
|
||
className={cn(formatDuration(totalTime) === '0ms' && 'text-foreground-lighter')}
|
||
>
|
||
{formatDuration(totalTime)}
|
||
</span>
|
||
</span>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
},
|
||
{
|
||
key: 'calls',
|
||
name: 'Calls',
|
||
minWidth: 90,
|
||
resizable: true,
|
||
headerCellClass: 'cursor-default',
|
||
renderHeaderCell: () => (
|
||
<div className="flex items-center text-xs w-full">
|
||
<p className="text-foreground! font-medium">Calls</p>
|
||
</div>
|
||
),
|
||
renderCell: (props) => {
|
||
const value = (props.row as ClassifiedQuery).calls
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
||
{value.toLocaleString()}
|
||
</p>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
},
|
||
{
|
||
key: 'mean_time',
|
||
name: 'Mean time',
|
||
minWidth: 90,
|
||
resizable: true,
|
||
headerCellClass: 'cursor-default',
|
||
renderHeaderCell: () => (
|
||
<div className="flex items-center text-xs w-full">
|
||
<p className="text-foreground! font-medium">Mean time</p>
|
||
</div>
|
||
),
|
||
renderCell: (props) => {
|
||
const value = (props.row as ClassifiedQuery).mean_time
|
||
return (
|
||
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
||
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
||
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
||
{formatDuration(value)}
|
||
</p>
|
||
) : (
|
||
<p className="text-muted">–</p>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
},
|
||
{
|
||
key: 'actions',
|
||
name: 'Actions',
|
||
minWidth: 200,
|
||
resizable: false,
|
||
headerCellClass: 'cursor-default',
|
||
renderHeaderCell: () => (
|
||
<div className="flex items-center text-xs w-full">
|
||
<p className="text-foreground! font-medium">Actions</p>
|
||
</div>
|
||
),
|
||
renderCell: (props) => {
|
||
const row = props.row as ClassifiedQuery
|
||
return (
|
||
<div className="flex items-center gap-2 justify-end w-full h-full">
|
||
{!row.issueType && (
|
||
<Button
|
||
variant="default"
|
||
size="tiny"
|
||
onClick={(e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
handleGoToLogs()
|
||
}}
|
||
>
|
||
Go to Logs
|
||
</Button>
|
||
)}
|
||
{row.issueType === 'index' && (
|
||
<div className="flex items-center" onClick={(e) => e.stopPropagation()}>
|
||
<Button
|
||
variant="primary"
|
||
size="tiny"
|
||
className="rounded-r-none border-r-0 focus-visible:z-10"
|
||
onClick={() => {
|
||
setSelectedTriageRow(props.rowIdx)
|
||
setSheetView('indexes')
|
||
}}
|
||
>
|
||
Create Index
|
||
</Button>
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="primary"
|
||
size="tiny"
|
||
className="rounded-l-none px-1 focus-visible:z-10"
|
||
icon={<ChevronDown size={12} />}
|
||
/>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-40">
|
||
<DropdownMenuItem onClick={() => handleGoToLogs()} className="gap-2">
|
||
<ExternalLink size={14} />
|
||
Go to Logs
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem
|
||
onClick={() => {
|
||
setSelectedTriageRow(props.rowIdx)
|
||
setSheetView('explain')
|
||
}}
|
||
className="gap-2"
|
||
>
|
||
<ScanSearch size={14} />
|
||
Explain
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
)}
|
||
{(row.issueType === 'error' || row.issueType === 'slow') && (
|
||
<div onClick={(e) => e.stopPropagation()}>
|
||
<AiAssistantDropdown
|
||
label="Fix with AI"
|
||
buildPrompt={() => buildQueryInsightFixPrompt(row).prompt}
|
||
onOpenAssistant={() => handleAiSuggestedFix(row)}
|
||
copyLabel="Copy Markdown"
|
||
additionalDropdownItems={[
|
||
{
|
||
label: 'Go to Logs',
|
||
icon: <ExternalLink size={14} />,
|
||
onClick: () => handleGoToLogs(),
|
||
},
|
||
...(row.issueType === 'slow'
|
||
? [
|
||
{
|
||
label: 'Explain',
|
||
icon: <ScanSearch size={14} />,
|
||
onClick: () => {
|
||
setSelectedTriageRow(props.rowIdx)
|
||
setSheetView('explain')
|
||
},
|
||
},
|
||
]
|
||
: []),
|
||
]}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
},
|
||
},
|
||
],
|
||
[
|
||
triageQueryColWidth,
|
||
timeConsumedWidth,
|
||
handleGoToLogs,
|
||
handleAiSuggestedFix,
|
||
setSelectedTriageRow,
|
||
setSheetView,
|
||
]
|
||
)
|
||
|
||
return { columns, triageColumns }
|
||
}
|