mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
<img width="1454" height="786" alt="image" src="https://github.com/user-attachments/assets/995790b1-a242-4a61-a0a0-349de7234923" /> Stack 4/4 · previous: #50688 ## Problem Selecting several logs to copy or send to the Assistant meant using a separate checkbox column and a banner above the table. Clicking a row and checking a row were two different selections. ## Solution - Unified Logs uses `useTableRowSelection`, so a row click, Cmd/Ctrl-click, Shift-click, and the checkbox all act on one selection. The selection resets when the project or filters change, and a linked `?id=` still restores its log. - The level indicator and checkbox now share one column. The checkbox shows on hover, focus, or when the row is selected. - The detail panel shows every selected log: one log gets the tabs, and two or more show their combined JSON. Copy-as-JSON and "Explain with AI" move into the panel header (`LogSelectionActions`), replacing `RowSelectionHeader`. - New Shift+↑ / Shift+↓ shortcuts extend the selection, with a hint in the panel footer. Prev/next navigation follows display order and keeps the row in view. - `LogTypeIcon` no longer puts a nested button inside clickable rows. ## Review instructions 1. Click a log, then Shift-click another. The range should be selected and the panel should show the combined JSON. 2. Cmd-click to add or remove single logs. Use the copy button (or its shortcut) and "Explain with AI". 3. With the panel open, press Shift+↑ / Shift+↓ to grow and shrink the selection. 4. Change a filter. The selection should clear. ## Checklist - [ ] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary * **New Features** * Added multi-row selection in Unified Logs, including additive, range, checkbox, and keyboard selection. * Added bulk JSON viewing and copying, plus AI-assisted actions for selected logs. * Added Shift+Arrow shortcuts to extend selections and improved row navigation. * **Accessibility** * Improved labels and focus behavior for log controls and log type indicators. * **Bug Fixes** * Kept single-log Overview and Raw JSON views available alongside multi-log selection. * Applied metadata visibility settings to selected-log actions and disabled actions for invalid log data. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
560 lines
20 KiB
TypeScript
560 lines
20 KiB
TypeScript
import {
|
|
ColumnFiltersState,
|
|
getCoreRowModel,
|
|
getFacetedRowModel,
|
|
getFilteredRowModel,
|
|
getSortedRowModel,
|
|
getFacetedMinMaxValues as getTTableFacetedMinMaxValues,
|
|
getFacetedUniqueValues as getTTableFacetedUniqueValues,
|
|
Row,
|
|
SortingState,
|
|
Table,
|
|
useReactTable,
|
|
VisibilityState,
|
|
} from '@tanstack/react-table'
|
|
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useFeatureFlags, useFlag, useParams } from 'common'
|
|
import { Loader2, PanelLeftClose, PanelLeftOpen } from 'lucide-react'
|
|
import { useQueryStates } from 'nuqs'
|
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import {
|
|
Button,
|
|
ChartConfig,
|
|
cn,
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
useIsMobile,
|
|
} from 'ui'
|
|
|
|
import { RefreshButton } from '../../ui/DataTable/RefreshButton'
|
|
import { generateDynamicColumns, UNIFIED_LOGS_COLUMNS } from './components/Columns'
|
|
import { DownloadLogsButton } from './components/DownloadLogsButton'
|
|
import { LogsFilterBar } from './components/LogsFilterBar'
|
|
import { LogsListPanel } from './components/LogsListPanel'
|
|
import { TooltipLabel } from './components/TooltipLabel'
|
|
import { ServiceFlowPanel } from './ServiceFlowPanel'
|
|
import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
|
|
import { filterFields as defaultFilterFields } from './UnifiedLogs.fields'
|
|
import {
|
|
buildDefaultColumnFilters,
|
|
buildFilterSearchUpdate,
|
|
parseLogsFilterUrlParams,
|
|
} from './UnifiedLogs.filters'
|
|
import { useFilterSearchSync, useLiveMode, useResetFocus } from './UnifiedLogs.hooks'
|
|
import { isUserFilterUnreachable } from './UnifiedLogs.queries'
|
|
import { ColumnSchema } from './UnifiedLogs.schema'
|
|
import { QuerySearchParamsType } from './UnifiedLogs.types'
|
|
import {
|
|
gateLogTypeFilters,
|
|
gateLogTypeOptions,
|
|
getComputeLogsAvailability,
|
|
getFacetedUniqueValues,
|
|
getLevelRowClassName,
|
|
} from './UnifiedLogs.utils'
|
|
import { LEVELS } from '@/components/ui/DataTable/DataTable.constants'
|
|
import { Option } from '@/components/ui/DataTable/DataTable.types'
|
|
import { arrSome, inDateRange } from '@/components/ui/DataTable/DataTable.utils'
|
|
import { DataTableFilterControlsDrawer } from '@/components/ui/DataTable/DataTableFilters/DataTableFilterControlsDrawer'
|
|
import { DataTableInfinite } from '@/components/ui/DataTable/DataTableInfinite'
|
|
import { DataTableSideBarLayout } from '@/components/ui/DataTable/DataTableSideBarLayout'
|
|
import { DataTableViewOptions } from '@/components/ui/DataTable/DataTableViewOptions'
|
|
import { FilterSideBar } from '@/components/ui/DataTable/FilterSideBar'
|
|
import { LiveButton } from '@/components/ui/DataTable/LiveButton'
|
|
import { DataTableProvider } from '@/components/ui/DataTable/providers/DataTableProvider'
|
|
import { RowSelectionModifiers } from '@/components/ui/DataTable/rowSelection.utils'
|
|
import { TimelineChart } from '@/components/ui/DataTable/TimelineChart'
|
|
import { useTableRowSelection } from '@/components/ui/DataTable/useTableRowSelection'
|
|
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
|
|
import { useUnifiedLogsChartQuery } from '@/data/logs/unified-logs-chart-query'
|
|
import { useUnifiedLogsCountQuery } from '@/data/logs/unified-logs-count-query'
|
|
import { useUnifiedLogsInfiniteQuery } from '@/data/logs/unified-logs-infinite-query'
|
|
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
|
import { useShowMultigresLogs } from '@/hooks/misc/useShowMultigresLogs'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
|
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
|
|
|
export const CHART_CONFIG = {
|
|
success: {
|
|
label: <TooltipLabel level="success" />,
|
|
color: 'var(--chart-muted)',
|
|
},
|
|
warning: {
|
|
label: <TooltipLabel level="warning" />,
|
|
color: 'var(--chart-status-warning)',
|
|
},
|
|
error: {
|
|
label: <TooltipLabel level="error" />,
|
|
color: 'hsl(var(--destructive-default))',
|
|
},
|
|
} satisfies ChartConfig
|
|
|
|
export const UnifiedLogs = () => {
|
|
useResetFocus()
|
|
|
|
const { ref: projectRef } = useParams()
|
|
const track = useTrack()
|
|
const [search, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
|
|
const showMultigresLogs = useShowMultigresLogs()
|
|
const { hasLoaded: flagsLoaded } = useFeatureFlags()
|
|
const computeEnabled = !!useFlag('compute')
|
|
const computeAvailability = getComputeLogsAvailability({
|
|
isPlatform: IS_PLATFORM,
|
|
flagsLoaded,
|
|
computeEnabled,
|
|
})
|
|
const visibleSearchFilters = gateLogTypeFilters(search.filter, {
|
|
multigres: showMultigresLogs,
|
|
compute: computeAvailability.preserveComputeFilter,
|
|
})
|
|
|
|
const defaultColumnSorting = search.sort ? [search.sort] : []
|
|
const defaultColumnVisibility = { uuid: false }
|
|
const defaultColumnFilters = buildDefaultColumnFilters({
|
|
...search,
|
|
filter: visibleSearchFilters,
|
|
})
|
|
|
|
const [topBarHeight, setTopBarHeight] = useState(0)
|
|
const topBarRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
const observer = new ResizeObserver(() => {
|
|
const rect = topBarRef.current?.getBoundingClientRect()
|
|
if (rect) setTopBarHeight(rect.height)
|
|
})
|
|
const topBar = topBarRef.current
|
|
if (!topBar) return
|
|
observer.observe(topBar)
|
|
return () => observer.unobserve(topBar)
|
|
}, [])
|
|
|
|
const [sorting, setSorting] = useState<SortingState>(defaultColumnSorting)
|
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(defaultColumnFilters)
|
|
|
|
const [dock, setDock] = useLocalStorageQuery<'bottom' | 'right'>(
|
|
LOCAL_STORAGE_KEYS.UNIFIED_LOGS_DOCK,
|
|
'bottom'
|
|
)
|
|
|
|
const [columnVisibility, setColumnVisibility] = useLocalStorageQuery<VisibilityState>(
|
|
'data-table-visibility',
|
|
defaultColumnVisibility
|
|
)
|
|
const [columnOrder, setColumnOrder] = useLocalStorageQuery<string[]>(
|
|
'data-table-column-order',
|
|
[]
|
|
)
|
|
|
|
// Create a stable query key object by removing nulls/undefined, id, and live
|
|
// Mainly to prevent the react queries from unnecessarily re-fetching
|
|
const searchParameters = useMemo(() => {
|
|
const parameters = Object.entries(search).reduce(
|
|
(acc, [key, value]) => {
|
|
if (!['id', 'live'].includes(key) && value !== null && value !== undefined) {
|
|
acc[key] = value
|
|
}
|
|
return acc
|
|
},
|
|
{} as Record<string, unknown>
|
|
) as QuerySearchParamsType
|
|
|
|
if (parameters.filter) {
|
|
parameters.filter =
|
|
gateLogTypeFilters(parameters.filter, {
|
|
multigres: showMultigresLogs,
|
|
compute: computeAvailability.canQueryCompute,
|
|
}) ?? null
|
|
}
|
|
return parameters
|
|
}, [search, showMultigresLogs, computeAvailability.canQueryCompute])
|
|
|
|
const { selection, selectRow, clearSelection } = useTableRowSelection({
|
|
scope: JSON.stringify([projectRef, searchParameters]),
|
|
initialId: search.id ?? undefined,
|
|
})
|
|
const rowSelection = selection.selected
|
|
|
|
const {
|
|
data: unifiedLogsData,
|
|
error,
|
|
isError,
|
|
isLoading,
|
|
isFetching,
|
|
isFetchingNextPage,
|
|
isFetchingPreviousPage,
|
|
hasNextPage,
|
|
refetch: refetchLogs,
|
|
fetchNextPage,
|
|
fetchPreviousPage,
|
|
} = useUnifiedLogsInfiniteQuery({ projectRef, search: searchParameters })
|
|
|
|
const {
|
|
data: counts,
|
|
isPending: isLoadingCounts,
|
|
isFetching: isFetchingCounts,
|
|
refetch: refetchCounts,
|
|
} = useUnifiedLogsCountQuery({
|
|
projectRef,
|
|
search: searchParameters,
|
|
})
|
|
|
|
const {
|
|
data: unifiedLogsChart = [],
|
|
isFetching: isFetchingCharts,
|
|
refetch: refetchCharts,
|
|
} = useUnifiedLogsChartQuery({
|
|
projectRef,
|
|
search: searchParameters,
|
|
})
|
|
|
|
const refetchAllData = () => {
|
|
refetchLogs()
|
|
refetchCounts()
|
|
refetchCharts()
|
|
}
|
|
|
|
const isRefetchingData = isFetching || isFetchingCounts || isFetchingCharts
|
|
|
|
// Only fade when filtering (not when loading more data or live mode)
|
|
const isFetchingButNotPaginating = isFetching && !isFetchingNextPage && !isFetchingPreviousPage
|
|
|
|
const rawFlatData = useMemo(() => {
|
|
return unifiedLogsData?.pages?.flatMap((page) => page.data ?? []) ?? []
|
|
}, [unifiedLogsData?.pages])
|
|
// [Joshen] Refer to unified-logs-infinite-query on why the need to deupe
|
|
const flatData = useMemo(() => {
|
|
return rawFlatData.filter((value, idx) => {
|
|
return idx === rawFlatData.findIndex((x) => x.id === value.id)
|
|
})
|
|
}, [rawFlatData])
|
|
const liveMode = useLiveMode(flatData)
|
|
|
|
const totalDBRowCount = counts?.totalRowCount
|
|
const filterDBRowCount = flatData.length
|
|
|
|
const facets = counts?.facets
|
|
const totalFetched = flatData?.length
|
|
|
|
// Create a filtered version of the chart config based on level filters in the URL.
|
|
const filteredChartConfig = useMemo(() => {
|
|
const levelFilters = parseLogsFilterUrlParams(search.filter).filter((f) => f.column === 'level')
|
|
const included = levelFilters.filter((f) => f.operator === '=').map((f) => f.value)
|
|
const excluded = new Set(levelFilters.filter((f) => f.operator === '<>').map((f) => f.value))
|
|
const baseLevels: readonly string[] = included.length > 0 ? included : LEVELS
|
|
const activeLevels = baseLevels.filter((l) => !excluded.has(l))
|
|
return Object.fromEntries(
|
|
Object.entries(CHART_CONFIG).filter(([key]) => activeLevels.includes(key))
|
|
) as ChartConfig
|
|
}, [search.filter])
|
|
|
|
const getRowClassName = <
|
|
TData extends { date: Date; level: (typeof LEVELS)[number] | null; timestamp: number },
|
|
>(
|
|
row: Row<TData>
|
|
) => {
|
|
const rowTimestamp = row.original.timestamp
|
|
const isPast = rowTimestamp <= (liveMode.timestamp || -1)
|
|
const levelClassName = getLevelRowClassName(row.original.level)
|
|
return cn(levelClassName, isPast ? 'opacity-50' : 'opacity-100', 'h-[30px]')
|
|
}
|
|
|
|
// Generate dynamic columns based on current data
|
|
const { columns: dynamicColumns, columnVisibility: dynamicColumnVisibility } = useMemo(() => {
|
|
return generateDynamicColumns({ data: flatData })
|
|
}, [flatData])
|
|
|
|
const table: Table<ColumnSchema> = useReactTable({
|
|
data: flatData,
|
|
columns: dynamicColumns,
|
|
state: {
|
|
columnFilters,
|
|
sorting,
|
|
columnVisibility: { ...dynamicColumnVisibility, ...columnVisibility },
|
|
rowSelection,
|
|
columnOrder,
|
|
},
|
|
enableMultiRowSelection: true,
|
|
columnResizeMode: 'onChange',
|
|
filterFns: { inDateRange, arrSome },
|
|
meta: { getRowClassName },
|
|
getRowId: (row) => row.id,
|
|
onColumnVisibilityChange: setColumnVisibility,
|
|
onColumnFiltersChange: setColumnFilters,
|
|
onSortingChange: setSorting,
|
|
onColumnOrderChange: setColumnOrder,
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getFacetedRowModel: getFacetedRowModel(),
|
|
getFacetedUniqueValues: getTTableFacetedUniqueValues(),
|
|
getFacetedMinMaxValues: getTTableFacetedMinMaxValues(),
|
|
})
|
|
|
|
const selectedRows = table.getSelectedRowModel().rows
|
|
const selectedRow =
|
|
selectedRows.find((row) => row.id === selection.activeId) ?? selectedRows.at(-1)
|
|
const openRowId = selectedRow?.id
|
|
const handleSelectRow = (id: string, modifiers?: RowSelectionModifiers) => {
|
|
selectRow(
|
|
table.getRowModel().rows.map((row) => row.id),
|
|
id,
|
|
modifiers
|
|
)
|
|
}
|
|
const setOpenRowId = (id: string | undefined) => {
|
|
if (id) handleSelectRow(id)
|
|
else clearSelection()
|
|
}
|
|
|
|
// Will need to refactor this bit
|
|
// - Each facet just handles its own state, rather than getting passed down like this
|
|
const filterFields = useMemo(() => {
|
|
const gatedFields = gateLogTypeOptions(defaultFilterFields, {
|
|
multigres: showMultigresLogs,
|
|
compute: computeAvailability.canQueryCompute,
|
|
})
|
|
|
|
return gatedFields.map((field) => {
|
|
const facetsField = facets?.[field.value]
|
|
|
|
// If no facets data available, use the predefined field
|
|
if (!facetsField) return field
|
|
|
|
// For hardcoded enum fields, keep the predefined options (facets only used for counts)
|
|
if (field.value === 'log_type' || field.value === 'method' || field.value === 'level') {
|
|
const fieldWithCounts = {
|
|
...field,
|
|
options: field.options.map((x) => {
|
|
return { ...x, count: facetsField.rows.find((y) => y.value === x.value)?.total ?? 0 }
|
|
}),
|
|
}
|
|
return fieldWithCounts
|
|
}
|
|
|
|
// For dynamic fields, use faceted options
|
|
const options: Option[] = facetsField.rows.map(({ value, total }) => ({
|
|
label: `${value}`,
|
|
value,
|
|
count: total,
|
|
}))
|
|
|
|
return { ...field, options }
|
|
})
|
|
}, [facets, showMultigresLogs, computeAvailability.canQueryCompute])
|
|
|
|
const applyFilterSearch = () => {
|
|
const update = buildFilterSearchUpdate(columnFilters, filterFields)
|
|
if (Array.isArray(update.filter)) {
|
|
update.filter = gateLogTypeFilters(update.filter.map(String), {
|
|
multigres: showMultigresLogs,
|
|
compute: computeAvailability.canQueryCompute,
|
|
})
|
|
}
|
|
setSearch(update)
|
|
}
|
|
|
|
useFilterSearchSync({
|
|
applyFilterSearch,
|
|
columnFilters,
|
|
enabled: computeAvailability.readyToSyncFilters,
|
|
})
|
|
|
|
useEffect(() => {
|
|
setSearch({ sort: sorting?.[0] || null })
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [sorting])
|
|
|
|
useEffect(() => {
|
|
if (isLoading || isFetching) return
|
|
|
|
if (openRowId && !selectedRow) {
|
|
// Clear both uuid and logId when the open row no longer exists in data
|
|
setSearch({ id: null })
|
|
setOpenRowId(undefined)
|
|
} else if (openRowId && selectedRow) {
|
|
setSearch({ id: openRowId })
|
|
track('unified_logs_row_clicked', { logType: selectedRow.original.log_type })
|
|
} else if (!openRowId && search.id) {
|
|
// Clear the URL parameter when no row is open
|
|
setSearch({ id: null })
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [openRowId, selectedRow, isLoading, isFetching])
|
|
|
|
const isMobile = useIsMobile()
|
|
const [isFilterBarOpen, setIsFilterBarOpen] = useState(!isMobile)
|
|
|
|
useShortcut(SHORTCUT_IDS.DATA_TABLE_TOGGLE_FILTERS, () => setIsFilterBarOpen((prev) => !prev), {
|
|
registerInCommandMenu: true,
|
|
})
|
|
useShortcut(SHORTCUT_IDS.UNIFIED_LOGS_CLEAR_FILTERS, () => table.resetColumnFilters(), {
|
|
enabled: columnFilters.length > 0,
|
|
registerInCommandMenu: true,
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (isMobile) {
|
|
setIsFilterBarOpen(false)
|
|
} else {
|
|
setIsFilterBarOpen(true)
|
|
}
|
|
}, [isMobile])
|
|
|
|
return (
|
|
<DataTableProvider
|
|
table={table}
|
|
error={error}
|
|
columns={UNIFIED_LOGS_COLUMNS}
|
|
filterFields={filterFields}
|
|
columnFilters={columnFilters}
|
|
sorting={sorting}
|
|
rowSelection={rowSelection}
|
|
openRowId={openRowId}
|
|
setOpenRowId={setOpenRowId}
|
|
onSelectRow={handleSelectRow}
|
|
columnOrder={columnOrder}
|
|
columnVisibility={columnVisibility}
|
|
searchParameters={searchParameters}
|
|
enableColumnOrdering={true}
|
|
isFetching={isFetching}
|
|
isError={isError}
|
|
isLoading={isLoading}
|
|
isLoadingCounts={isLoadingCounts}
|
|
getFacetedUniqueValues={getFacetedUniqueValues(facets)}
|
|
>
|
|
<DataTableSideBarLayout topBarHeight={topBarHeight}>
|
|
<ResizablePanelGroup orientation="horizontal" autoSaveId="logs-layout">
|
|
<FilterSideBar
|
|
isFilterBarOpen={isFilterBarOpen}
|
|
setIsFilterBarOpen={setIsFilterBarOpen}
|
|
dateRangeDisabled={{ after: new Date() }}
|
|
/>
|
|
<ResizableHandle withHandle />
|
|
<ResizablePanel
|
|
id="panel-right"
|
|
className="flex max-w-full flex-1 flex-col overflow-hidden"
|
|
>
|
|
<div ref={topBarRef} className="top-0 z-10 flex flex-col bg-background">
|
|
<div className="flex flex-wrap items-center gap-2 px-2 border-b">
|
|
<ShortcutTooltip shortcutId={SHORTCUT_IDS.DATA_TABLE_TOGGLE_FILTERS} side="bottom">
|
|
<Button
|
|
size="tiny"
|
|
variant="text"
|
|
icon={isFilterBarOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
|
onClick={() => setIsFilterBarOpen((prev) => !prev)}
|
|
className="hidden w-[26px] sm:flex"
|
|
aria-label={isFilterBarOpen ? 'Hide filters' : 'Show filters'}
|
|
/>
|
|
</ShortcutTooltip>
|
|
|
|
<div className="h-full border-r" />
|
|
|
|
<div className="order-first w-full min-w-0 sm:order-0 sm:w-auto sm:flex-1 py-2">
|
|
<LogsFilterBar />
|
|
</div>
|
|
|
|
<div className="block sm:hidden">
|
|
<DataTableFilterControlsDrawer />
|
|
</div>
|
|
|
|
<div className="ml-auto flex items-center gap-x-2">
|
|
<RefreshButton
|
|
isLoading={isRefetchingData}
|
|
onRefresh={refetchAllData}
|
|
shortcutId={SHORTCUT_IDS.UNIFIED_LOGS_REFRESH}
|
|
/>
|
|
<DataTableViewOptions />
|
|
<DownloadLogsButton searchParameters={searchParameters} />
|
|
{fetchPreviousPage ? (
|
|
<LiveButton
|
|
fetchPreviousPage={fetchPreviousPage}
|
|
searchParamsParser={SEARCH_PARAMS_PARSER}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="h-[60px] flex items-center justify-center">
|
|
<Loader2 size={14} className="animate-spin text-foreground-lighter" />
|
|
</div>
|
|
) : (
|
|
<TimelineChart
|
|
data={unifiedLogsChart}
|
|
className={cn(
|
|
'-mb-1.5 mt-1.5',
|
|
isFetchingCharts && 'opacity-60 transition-opacity duration-150'
|
|
)}
|
|
columnId="timestamp"
|
|
filterColumnId="date"
|
|
chartConfig={filteredChartConfig}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<ResizablePanelGroup
|
|
key="main-logs"
|
|
className="flex-1 border-t"
|
|
orientation={dock === 'bottom' ? 'vertical' : 'horizontal'}
|
|
>
|
|
<ResizablePanel
|
|
defaultSize="100"
|
|
minSize="10"
|
|
className={cn(
|
|
'bg',
|
|
isFetchingButNotPaginating && 'opacity-60 transition-opacity duration-150'
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'h-full [&>div]:h-full',
|
|
'[&_thead_th]:[border-top:none]! [&_thead_th]:[border-bottom:none]!',
|
|
'[&_thead_th]:[box-shadow:inset_0_-1px_0_var(--border-default)]!',
|
|
'[&_thead_th]:text-foreground-lighter! [&_thead_tr]:bg-background! [&_thead_tr:hover]:bg-background!',
|
|
'[&_thead_tr]:border-b-0! [&_tbody_tr]:border-b-0!'
|
|
)}
|
|
>
|
|
<DataTableInfinite
|
|
columns={UNIFIED_LOGS_COLUMNS}
|
|
totalRows={totalDBRowCount}
|
|
filterRows={filterDBRowCount}
|
|
totalRowsFetched={totalFetched}
|
|
fetchNextPage={fetchNextPage}
|
|
hasNextPage={hasNextPage}
|
|
setColumnOrder={setColumnOrder}
|
|
setColumnVisibility={setColumnVisibility}
|
|
errorSubject="Failed to retrieve logs"
|
|
emptyStateMessage={
|
|
isUserFilterUnreachable(searchParameters) ? (
|
|
<div className="text-sm flex flex-col gap-y-1">
|
|
<p className="text-foreground-light">No results found</p>
|
|
<p className="text-foreground-lighter">
|
|
Filtering by user is only supported for Auth and API Gateway log types
|
|
</p>
|
|
</div>
|
|
) : undefined
|
|
}
|
|
/>
|
|
</div>
|
|
</ResizablePanel>
|
|
|
|
{!!openRowId && !!selectedRow && (
|
|
<>
|
|
{selectedRows.length === 1 && <LogsListPanel selectedRow={selectedRow} />}
|
|
<ServiceFlowPanel
|
|
dock={dock}
|
|
setDock={setDock}
|
|
selectedRows={selectedRows.map((row) => row.original)}
|
|
searchParameters={searchParameters}
|
|
/>
|
|
</>
|
|
)}
|
|
</ResizablePanelGroup>
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</DataTableSideBarLayout>
|
|
</DataTableProvider>
|
|
)
|
|
}
|