| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- import {
- ColumnFiltersState,
- getCoreRowModel,
- getFacetedRowModel,
- getFilteredRowModel,
- getSortedRowModel,
- getFacetedMinMaxValues as getTTableFacetedMinMaxValues,
- getFacetedUniqueValues as getTTableFacetedUniqueValues,
- Row,
- RowSelectionState,
- SortingState,
- Table,
- useReactTable,
- VisibilityState,
- } from '@tanstack/react-table'
- import { LOCAL_STORAGE_KEYS, useDebounce, useParams } from 'common'
- import { 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 { RowSelectionHeader } from './RowSelectionHeader'
- import { ServiceFlowPanel } from './ServiceFlowPanel'
- import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
- import { filterFields as defaultFilterFields } from './UnifiedLogs.fields'
- import { useLiveMode, useResetFocus } from './UnifiedLogs.hooks'
- import { ColumnSchema } from './UnifiedLogs.schema'
- import { QuerySearchParamsType } from './UnifiedLogs.types'
- import { 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 { TimelineChart } from '@/components/ui/DataTable/TimelineChart'
- 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 { 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: 'hsl(var(--foreground-muted))',
- },
- warning: {
- label: <TooltipLabel level="warning" />,
- color: 'hsl(var(--warning-default))',
- },
- 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 { sort, start, size, id, cursor, direction, live, ...filter } = search
- const defaultColumnSorting = sort ? [sort] : []
- const defaultColumnVisibility = { uuid: false }
- const defaultColumnFilters = Object.entries(filter)
- .map(([key, value]) => ({ id: key, value }))
- .filter(({ value }) => value ?? undefined)
- 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 [rowSelection, setRowSelection] = useState<RowSelectionState>({})
- const [openRowId, setOpenRowId] = useState<string | undefined>(search.id ?? undefined)
- 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(
- () =>
- 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,
- [search]
- )
- 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 selected levels
- const filteredChartConfig = useMemo(() => {
- const levelFilter = search.level || LEVELS
- return Object.fromEntries(
- Object.entries(CHART_CONFIG).filter(([key]) =>
- levelFilter.includes(key as (typeof LEVELS)[number])
- )
- ) as ChartConfig
- }, [search.level])
- const getRowClassName = <
- TData extends { date: Date; level: (typeof LEVELS)[number]; 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,
- onRowSelectionChange: setRowSelection,
- onSortingChange: setSorting,
- onColumnOrderChange: setColumnOrder,
- getSortedRowModel: getSortedRowModel(),
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getFacetedRowModel: getFacetedRowModel(),
- getFacetedUniqueValues: getTTableFacetedUniqueValues(),
- getFacetedMinMaxValues: getTTableFacetedMinMaxValues(),
- })
- const selectedRow = useMemo(() => {
- if ((isLoading || isFetching) && !flatData.length) return
- return table.getCoreRowModel().flatRows.find((row) => row.id === openRowId)
- }, [isLoading, isFetching, flatData.length, table, openRowId])
- // REMINDER: this is currently needed for the cmdk search
- // [Joshen] This is where facets are getting dynamically loaded
- // TODO: auto search via API when the user changes the filter instead of hardcoded
- // Will need to refactor this bit
- // - Each facet just handles its own state, rather than getting passed down like this
- const filterFields = useMemo(() => {
- return defaultFilterFields.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') {
- return field
- }
- // For dynamic fields, use faceted options
- const options: Option[] = facetsField.rows.map(({ value }) => ({
- label: `${value}`,
- value,
- }))
- return { ...field, options }
- })
- }, [facets])
- // Debounced filter application to avoid too many API calls when user clicks multiple filters quickly
- const applyFilterSearch = () => {
- const columnFiltersWithNullable = filterFields.map((field) => {
- const filterValue = columnFilters.find((filter) => filter.id === field.value)
- if (!filterValue) return { id: field.value, value: null }
- return { id: field.value, value: filterValue.value }
- })
- const search = columnFiltersWithNullable.reduce(
- (prev, curr) => {
- // Add to search parameters
- prev[curr.id as string] = curr.value
- return prev
- },
- {} as Record<string, unknown>
- )
- setSearch(search)
- }
- const debouncedApplyFilterSearch = useDebounce(applyFilterSearch, 250)
- useEffect(() => {
- debouncedApplyFilterSearch()
- }, [columnFilters, debouncedApplyFilterSearch])
- 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))
- useEffect(() => {
- if (isMobile) {
- setIsFilterBarOpen(false)
- } else {
- setIsFilterBarOpen(true)
- }
- }, [isMobile])
- useEffect(() => {
- table.resetRowSelection()
- }, [searchParameters, table])
- return (
- <DataTableProvider
- table={table}
- error={error}
- columns={UNIFIED_LOGS_COLUMNS}
- filterFields={filterFields}
- columnFilters={columnFilters}
- sorting={sorting}
- rowSelection={rowSelection}
- openRowId={openRowId}
- setOpenRowId={setOpenRowId}
- 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"
- type="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} />
- <DataTableViewOptions />
- <DownloadLogsButton searchParameters={searchParameters} />
- {fetchPreviousPage ? (
- <LiveButton
- fetchPreviousPage={fetchPreviousPage}
- searchParamsParser={SEARCH_PARAMS_PARSER}
- />
- ) : null}
- </div>
- </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>
- <RowSelectionHeader />
- <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_hsl(var(--border-default))]!',
- '[&_thead_th]:text-foreground-lighter! [&_thead_tr:hover]:bg-surface-75',
- '[&_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}
- searchParamsParser={SEARCH_PARAMS_PARSER}
- />
- </div>
- </ResizablePanel>
- {!!openRowId && !!selectedRow && (
- <>
- <LogsListPanel selectedRow={selectedRow} />
- <ServiceFlowPanel
- dock={dock}
- setDock={setDock}
- selectedRow={selectedRow?.original}
- selectedRowKey={openRowId}
- searchParameters={searchParameters}
- />
- </>
- )}
- </ResizablePanelGroup>
- </ResizablePanel>
- </ResizablePanelGroup>
- </DataTableSideBarLayout>
- </DataTableProvider>
- )
- }
|