diff --git a/apps/studio/components/interfaces/logs-new/LogTypeIcon.tsx b/apps/studio/components/interfaces/UnifiedLogs/LogTypeIcon.tsx
similarity index 97%
rename from apps/studio/components/interfaces/logs-new/LogTypeIcon.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/LogTypeIcon.tsx
index 728052651a6..91dadb4a4cc 100644
--- a/apps/studio/components/interfaces/logs-new/LogTypeIcon.tsx
+++ b/apps/studio/components/interfaces/UnifiedLogs/LogTypeIcon.tsx
@@ -3,7 +3,7 @@
import { Auth, EdgeFunctions, Storage } from 'icons'
import { BookHeart, Box, Cpu, Database, Globe } from 'lucide-react'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
-import { type LOG_TYPES } from './schema'
+import { type LOG_TYPES } from './UnifiedLogs.schema'
interface LogTypeIconProps {
type: (typeof LOG_TYPES)[number]
diff --git a/apps/studio/components/interfaces/UnifiedLogs/TooltipLabel.tsx b/apps/studio/components/interfaces/UnifiedLogs/TooltipLabel.tsx
new file mode 100644
index 00000000000..b29ae4c7946
--- /dev/null
+++ b/apps/studio/components/interfaces/UnifiedLogs/TooltipLabel.tsx
@@ -0,0 +1,10 @@
+import { getLevelLabel } from './UnifiedLogs.utils'
+
+export const TooltipLabel = ({ level }: { level: 'success' | 'warning' | 'error' }) => {
+ return (
+
+
{level}
+
{getLevelLabel(level)}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/logs-new/search-params.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx
similarity index 58%
rename from apps/studio/components/interfaces/logs-new/search-params.ts
rename to apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx
index 78711ac0513..00636b8a0f7 100644
--- a/apps/studio/components/interfaces/logs-new/search-params.ts
+++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx
@@ -1,30 +1,42 @@
import {
createParser,
- // createSearchParamsCache, // Removed for Pages Router compatibility
- createSerializer, // Uncommented for Pages Router compatibility
parseAsArrayOf,
parseAsBoolean,
parseAsInteger,
parseAsString,
parseAsStringLiteral,
parseAsTimestamp,
- type inferParserType,
- useQueryStates, // Added for potential hook usage
} from 'nuqs'
-// Note: import from 'nuqs/server' comment removed
-import {
- ARRAY_DELIMITER,
- RANGE_DELIMITER,
- SLIDER_DELIMITER,
- SORT_DELIMITER,
-} from 'components/interfaces/DataTableDemo/lib/delimiters'
-import { REGIONS } from 'components/interfaces/DataTableDemo/constants/region'
-import { METHODS } from 'components/interfaces/DataTableDemo/constants/method'
-import { LEVELS } from 'components/interfaces/DataTableDemo/constants/levels'
-// https://logs.run/i?sort=latency.desc
+import { ChartConfig } from 'ui'
+import { TooltipLabel } from './TooltipLabel'
-export const parseAsSort = createParser({
+export const CHART_CONFIG = {
+ success: {
+ label: ,
+ color: 'hsl(var(--foreground-muted))',
+ },
+ warning: {
+ label: ,
+ color: 'hsl(var(--warning-default))',
+ },
+ error: {
+ label: ,
+ color: 'hsl(var(--destructive-default))',
+ },
+} satisfies ChartConfig
+
+export const ARRAY_DELIMITER = ','
+const SLIDER_DELIMITER = '-'
+const SPACE_DELIMITER = '_'
+export const RANGE_DELIMITER = '-'
+export const SORT_DELIMITER = '.'
+
+const LEVELS = ['success', 'warning', 'error'] as const
+const REGIONS = ['ams', 'fra', 'gru', 'hkg', 'iad', 'syd'] as const
+const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const
+
+const parseAsSort = createParser({
parse(queryValue) {
const [id, desc] = queryValue.split(SORT_DELIMITER)
if (!id && !desc) return null
@@ -35,7 +47,7 @@ export const parseAsSort = createParser({
},
})
-export const searchParamsParser = {
+export const SEARCH_PARAMS_PARSER = {
// CUSTOM FILTERS
level: parseAsArrayOf(parseAsStringLiteral(LEVELS), ARRAY_DELIMITER),
log_type: parseAsArrayOf(parseAsString, ARRAY_DELIMITER),
@@ -62,14 +74,3 @@ export const searchParamsParser = {
// REQUIRED FOR SELECTION
uuid: parseAsString,
}
-
-// Removed for Pages Router compatibility
-// export const searchParamsCache = createSearchParamsCache(searchParamsParser)
-export const searchParamsSerializer = createSerializer(searchParamsParser) // Uncommented
-
-export type SearchParamsType = inferParserType
-
-// Optional: Add a hook similar to the default demo if needed elsewhere
-// export function useInfiniteSearchParams() {
-// return useQueryStates(searchParamsParser)
-// }
diff --git a/apps/studio/components/interfaces/logs-new/schema.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.schema.ts
similarity index 57%
rename from apps/studio/components/interfaces/logs-new/schema.ts
rename to apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.schema.ts
index baae35c770d..b29e630f4ba 100644
--- a/apps/studio/components/interfaces/logs-new/schema.ts
+++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.schema.ts
@@ -1,34 +1,9 @@
-import {
- ARRAY_DELIMITER,
- RANGE_DELIMITER,
- SLIDER_DELIMITER,
-} from 'components/interfaces/DataTableDemo/lib/delimiters'
+import { z } from 'zod'
+
+import { LEVELS } from './constants/levels'
import { METHODS } from './constants/method'
import { REGIONS } from './constants/region'
-import { z } from 'zod'
-import { LEVELS } from './constants/levels'
-
-// https://github.com/colinhacks/zod/issues/2985#issue-2008642190
-const stringToBoolean = z
- .string()
- .toLowerCase()
- .transform((val) => {
- try {
- return JSON.parse(val)
- } catch (e) {
- console.log(e)
- return undefined
- }
- })
- .pipe(z.boolean().optional())
-
-// export const timingSchema = z.object({
-// 'timing.dns': z.number(),
-// 'timing.connection': z.number(),
-// 'timing.tls': z.number(),
-// 'timing.ttfb': z.number(),
-// 'timing.transfer': z.number(),
-// })
+import { ARRAY_DELIMITER, RANGE_DELIMITER } from './UnifiedLogs.constants'
export const LOG_TYPES = [
'edge',
@@ -50,26 +25,18 @@ export const columnSchema = z.object({
host: z.string(),
pathname: z.string(),
level: z.enum(LEVELS),
- // latency: z.number(),
status: z.number(),
- // regions: z.enum(REGIONS).array(),
date: z.date(),
- // headers: z.record(z.string()),
- // message: z.string().optional(),
timestamp: z.number(),
event_message: z.string().optional(),
log_count: z.number().optional(), // used to count function logs for a given execution_id
- // percentile: z.number().optional(),
has_trace: z.boolean().optional(),
logs: z.array(z.any()).optional(), // array of function logs
auth_user: z.string().optional(),
})
-// .merge(timingSchema)
export type ColumnSchema = z.infer
-// export type TimingSchema = z.infer
-// TODO: can we get rid of this in favor of nuqs search-params?
export const columnFilterSchema = z.object({
level: z
.string()
@@ -83,36 +50,6 @@ export const columnFilterSchema = z.object({
.optional(),
host: z.string().optional(),
pathname: z.string().optional(),
- // latency: z
- // .string()
- // .transform((val) => val.split(SLIDER_DELIMITER))
- // .pipe(z.coerce.number().array().max(2))
- // .optional(),
- // 'timing.dns': z
- // .string()
- // .transform((val) => val.split(SLIDER_DELIMITER))
- // .pipe(z.coerce.number().array().max(2))
- // .optional(),
- // 'timing.connection': z
- // .string()
- // .transform((val) => val.split(SLIDER_DELIMITER))
- // .pipe(z.coerce.number().array().max(2))
- // .optional(),
- // 'timing.tls': z
- // .string()
- // .transform((val) => val.split(SLIDER_DELIMITER))
- // .pipe(z.coerce.number().array().max(2))
- // .optional(),
- // 'timing.ttfb': z
- // .string()
- // .transform((val) => val.split(SLIDER_DELIMITER))
- // .pipe(z.coerce.number().array().max(2))
- // .optional(),
- // 'timing.transfer': z
- // .string()
- // .transform((val) => val.split(SLIDER_DELIMITER))
- // .pipe(z.coerce.number().array().max(2))
- // .optional(),
status: z
.string()
.transform((val) => val.split(ARRAY_DELIMITER))
diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.types.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.types.ts
new file mode 100644
index 00000000000..d1f345f3731
--- /dev/null
+++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.types.ts
@@ -0,0 +1,10 @@
+import { type inferParserType } from 'nuqs'
+
+import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
+
+export type UnifiedLogsMeta = {
+ logTypeCounts: Record
+ currentPercentiles: Record
+}
+
+export type SearchParamsType = inferParserType
diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts
new file mode 100644
index 00000000000..67324e1ca29
--- /dev/null
+++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts
@@ -0,0 +1,104 @@
+// @ts-ignore
+import { _LEVELS } from '@/constants/levels'
+import { type Table as TTable } from '@tanstack/react-table'
+import { useQueryState } from 'nuqs'
+
+import { useHotKey } from 'hooks/ui/useHotKey'
+import { useEffect, useMemo, useRef } from 'react'
+import { cn } from 'ui'
+import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
+import { FacetMetadataSchema } from './UnifiedLogs.schema'
+
+export const useResetFocus = () => {
+ useHotKey(() => {
+ // FIXME: some dedicated div[tabindex="0"] do not auto-unblur (e.g. the DataTableFilterResetButton)
+ // REMINDER: we cannot just document.activeElement?.blur(); as the next tab will focus the next element in line,
+ // which is not what we want. We want to reset entirely.
+ document.body.setAttribute('tabindex', '0')
+ document.body.focus()
+ document.body.removeAttribute('tabindex')
+ }, '.')
+}
+
+// TODO: make a BaseObject (incl. date and uuid e.g. for every upcoming branch of infinite table)
+export const useLiveMode = (data: TData[]) => {
+ const [live] = useQueryState('live', SEARCH_PARAMS_PARSER.live)
+ // REMINDER: used to capture the live mode on timestamp
+ const liveTimestamp = useRef(live ? new Date().getTime() : undefined)
+
+ useEffect(() => {
+ if (live) liveTimestamp.current = new Date().getTime()
+ else liveTimestamp.current = undefined
+ }, [live])
+
+ const anchorRow = useMemo(() => {
+ if (!live) return undefined
+
+ const item = data.find((item) => {
+ // return first item that is there if not liveTimestamp
+ if (!liveTimestamp.current) return true
+ // return first item that is after the liveTimestamp
+ if (item.date.getTime() > liveTimestamp.current) return false
+ return true
+ // return first item if no liveTimestamp
+ })
+
+ return item
+ }, [live, data])
+
+ return { row: anchorRow, timestamp: liveTimestamp.current }
+}
+
+export const getFacetedUniqueValues = (facets?: Record) => {
+ return (table: TTable, columnId: string) => {
+ return new Map(facets?.[columnId]?.rows?.map(({ value, total }) => [value, total]) || [])
+ }
+}
+
+export const getFacetedMinMaxValues = (facets?: Record) => {
+ return (table: TTable, columnId: string) => {
+ const min = facets?.[columnId]?.min
+ const max = facets?.[columnId]?.max
+ if (typeof min === 'number' && typeof max === 'number') return [min, max]
+ if (typeof min === 'number') return [min, min]
+ if (typeof max === 'number') return [max, max]
+ return undefined
+ }
+}
+
+export const getLevelLabel = (value: (typeof _LEVELS)[number]): string => {
+ switch (value) {
+ case 'success':
+ return '2xx'
+ case 'warning':
+ return '4xx'
+ case 'error':
+ return '5xx'
+ default:
+ return 'Unknown'
+ }
+}
+
+export function getLevelRowClassName(value: (typeof _LEVELS)[number]): string {
+ switch (value) {
+ case 'success':
+ return ''
+ case 'warning':
+ return cn(
+ 'bg-warning/5 hover:bg-warning/10 data-[state=selected]:bg-warning/20 focus-visible:bg-warning/10',
+ 'dark:bg-warning/10 dark:hover:bg-warning/20 dark:data-[state=selected]:bg-warning/30 dark:focus-visible:bg-warning/20'
+ )
+ case 'error':
+ return cn(
+ 'bg-destructive/5 hover:bg-destructive/10 data-[state=selected]:bg-destructive/20 focus-visible:bg-destructive/10',
+ 'dark:bg-error/10 dark:hover:bg-destructive/20 dark:data-[state=selected]:bg-destructive/30 dark:focus-visible:bg-destructive/20'
+ )
+ case 'info':
+ return cn(
+ 'bg-info/5 hover:bg-info/10 data-[state=selected]:bg-info/20 focus-visible:bg-info/10',
+ 'dark:bg-info/10 dark:hover:bg-info/20 dark:data-[state=selected]:bg-info/30 dark:focus-visible:bg-info/20'
+ )
+ default:
+ return ''
+ }
+}
diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogsPreviewer.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogsPreviewer.tsx
deleted file mode 100644
index 4d98010eebe..00000000000
--- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogsPreviewer.tsx
+++ /dev/null
@@ -1,251 +0,0 @@
-import dayjs from 'dayjs'
-import { Rewind } from 'lucide-react'
-import { useRouter } from 'next/router'
-import { PropsWithChildren, useEffect, useState } from 'react'
-
-import { useParams } from 'common'
-import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel'
-import LoadingOpacity from 'components/ui/LoadingOpacity'
-import ShimmerLine from 'components/ui/ShimmerLine'
-import useUnifiedLogs from 'hooks/analytics/useUnifiedLogs'
-import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState'
-import { useSelectedLog } from 'hooks/analytics/useSelectedLog'
-import useSingleLog from 'hooks/analytics/useSingleLog'
-import { useFlag } from 'hooks/ui/useFlag'
-import { Button } from 'ui'
-import { LogsBarChart } from 'ui-patterns/LogsBarChart'
-import LogTable from '../Settings/Logs/LogTable'
-import { DatePickerValue } from '../Settings/Logs/Logs.DatePickers'
-import { LogsTableName, PREVIEWER_DATEPICKER_HELPERS } from '../Settings/Logs/Logs.constants'
-import type {
- Filters,
- LogSearchCallback,
- LogTemplate,
- QueryType,
-} from '../Settings/Logs/Logs.types'
-import { PreviewFilterPanelWithUniversal } from '../Settings/Logs/PreviewFilterPanelWithUniversal'
-
-interface UnifiedLogsPreviewerProps {
- projectRef: string
- filterOverride?: Filters
- condensedLayout?: boolean
- EmptyState?: React.ReactNode
- filterPanelClassName?: string
-}
-
-export const UnifiedLogsPreviewer = ({
- projectRef,
- filterOverride,
- condensedLayout = false,
- children,
- EmptyState,
- filterPanelClassName,
-}: PropsWithChildren) => {
- const useUniversalFilterBar = useFlag('universalFilterBar')
- const router = useRouter()
- const { db } = useParams()
-
- const [showChart, setShowChart] = useState(true)
- const [selectedDatePickerValue, setSelectedDatePickerValue] = useState(
- getDefaultDatePickerValue()
- )
-
- const { search, setSearch, timestampStart, timestampEnd, setTimeRange, filters, setFilters } =
- useLogsUrlState()
- const [selectedLogId, setSelectedLogId] = useSelectedLog()
-
- // Use the custom useUnifiedLogs hook
- const {
- error,
- logData,
- params,
- newCount,
- isLoading,
- eventChartData,
- isLoadingOlder,
- loadOlder,
- refresh,
- } = useUnifiedLogs({ projectRef, filterOverride })
-
- const {
- data: selectedLog,
- isLoading: isSelectedLogLoading,
- error: selectedLogError,
- } = useSingleLog({
- projectRef,
- id: selectedLogId ?? undefined,
- queryType: 'unified',
- paramsToMerge: params,
- })
-
- // TODO: Move this to useLogsUrlState to simplify LogsPreviewer
- function getDefaultDatePickerValue() {
- const iso_timestamp_start = router.query.iso_timestamp_start as string
- const iso_timestamp_end = router.query.iso_timestamp_end as string
-
- if (iso_timestamp_start && iso_timestamp_end) {
- return {
- to: iso_timestamp_end,
- from: iso_timestamp_start,
- text: `${dayjs(iso_timestamp_start).format('DD MMM, HH:mm')} - ${dayjs(iso_timestamp_end).format('DD MMM, HH:mm')}`,
- isHelper: false,
- }
- }
-
- const defaultDatePickerValue = PREVIEWER_DATEPICKER_HELPERS.find((x) => x.default)
- return {
- to: defaultDatePickerValue!.calcTo(),
- from: defaultDatePickerValue!.calcFrom(),
- text: defaultDatePickerValue!.text,
- isHelper: true,
- }
- }
-
- const onSelectTemplate = (template: LogTemplate) => {
- setFilters({ ...filters, search_query: template.searchString })
- }
-
- // For helper date picker values, reset the timestamp start to prevent data caching
- const handleRefresh = () => {
- if (selectedDatePickerValue.isHelper) {
- const helper = PREVIEWER_DATEPICKER_HELPERS.find(
- (x) => x.text === selectedDatePickerValue.text
- )
- if (helper) {
- const newTimestampStart = helper.calcFrom()
- setTimeRange(newTimestampStart, timestampEnd)
- }
- }
- refresh()
- }
-
- const handleSearch: LogSearchCallback = async (event, { query, to, from }) => {
- if (event === 'search-input-change') {
- setSearch(query || '')
- setSelectedLogId(null)
- } else if (event === 'event-chart-bar-click') {
- setTimeRange(from || '', to || '')
- } else if (event === 'datepicker-change') {
- setTimeRange(from || '', to || '')
- }
- }
-
- // Common props shared between both filter panel components to avoid duplication
- const filterPanelProps = {
- className: filterPanelClassName,
- csvData: logData,
- isLoading,
- newCount,
- onRefresh: handleRefresh,
- onSearch: handleSearch,
- defaultSearchValue: search,
- defaultToValue: timestampEnd,
- defaultFromValue: timestampStart,
- queryUrl: `/project/${projectRef}/logs/explorer?q=${encodeURIComponent(
- params.sql || ''
- )}&its=${encodeURIComponent(timestampStart)}&ite=${encodeURIComponent(timestampEnd)}`,
- onSelectTemplate,
- filters,
- onFiltersChange: setFilters,
- table: LogsTableName.UNIFIED,
- condensedLayout,
- isShowingEventChart: showChart,
- onToggleEventChart: () => setShowChart(!showChart),
- onSelectedDatabaseChange: (id: string) => {
- setFilters({ ...filters, database: id !== projectRef ? id : undefined })
- const { db, ...params } = router.query
- router.push({
- pathname: router.pathname,
- query: id !== projectRef ? { ...router.query, db: id } : params,
- })
- },
- selectedDatePickerValue,
- setSelectedDatePickerValue,
- }
-
- return (
-
- {useUniversalFilterBar ? (
- // Experimental Universal Filter Bar
-
- ) : (
- // Legacy Filter Panel
-
- )}
- {children}
-
0 ? 'mb-2 mt-1 opacity-100' : 'h-0 opacity-0')
- }
- >
-
- {showChart && (
-
{
- if (!datum?.timestamp) return
-
- const datumTimestamp = dayjs(datum.timestamp).toISOString()
-
- const start = dayjs(datumTimestamp).subtract(1, 'minute').toISOString()
- const end = dayjs(datumTimestamp).add(1, 'minute').toISOString()
-
- handleSearch('event-chart-bar-click', {
- query: filters.search_query?.toString(),
- to: end,
- from: start,
- })
- }}
- EmptyState={
-
-
No data
-
- It may take up to 24 hours for data to refresh
-
-
- }
- />
- )}
-
-
-
-
-
- setShowChart(!showChart)}
- error={error}
- EmptyState={EmptyState}
- onSelectedLogChange={(log) => setSelectedLogId(log?.id ?? null)}
- selectedLog={selectedLog}
- isSelectedLogLoading={isSelectedLogLoading}
- selectedLogError={selectedLogError ?? undefined}
- />
-
-
- {!error && logData.length > 0 && (
-
-
}
- type="default"
- loading={isLoadingOlder}
- disabled={isLoadingOlder}
- >
- Load older
-
-
- Showing {logData.length} results
-
-
- )}
-
- )
-}
-
-export default UnifiedLogsPreviewer
diff --git a/apps/studio/components/interfaces/UnifiedLogs/client.tsx b/apps/studio/components/interfaces/UnifiedLogs/client.tsx
new file mode 100644
index 00000000000..0d91d16a946
--- /dev/null
+++ b/apps/studio/components/interfaces/UnifiedLogs/client.tsx
@@ -0,0 +1,445 @@
+import { useInfiniteQuery } from '@tanstack/react-query'
+import {
+ ColumnFiltersState,
+ getCoreRowModel,
+ getFacetedRowModel,
+ getFilteredRowModel,
+ getSortedRowModel,
+ getFacetedMinMaxValues as getTTableFacetedMinMaxValues,
+ getFacetedUniqueValues as getTTableFacetedUniqueValues,
+ Row,
+ RowSelectionState,
+ SortingState,
+ useReactTable,
+ VisibilityState,
+} from '@tanstack/react-table'
+import { useQueryStates } from 'nuqs'
+import { useEffect, useMemo, useState } from 'react'
+
+// import { useLocalStorage } from 'components/interfaces/DataTableDemo/hooks/use-local-storage'
+import { DataTableInfinite } from 'components/interfaces/data-table/data-table-infinite'
+import { TimelineChart } from 'components/interfaces/data-table/timeline-chart'
+import { DataTableFilterCommand } from 'components/interfaces/DataTableDemo/components/data-table/data-table-filter-command'
+import { DataTableProvider } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
+import { DataTableSheetDetails } from 'components/interfaces/DataTableDemo/components/data-table/data-table-sheet/data-table-sheet-details'
+import { DataTableToolbar } from 'components/interfaces/DataTableDemo/components/data-table/data-table-toolbar'
+import { LiveButton } from 'components/interfaces/DataTableDemo/infinite/_components/live-button'
+import { LiveRow } from 'components/interfaces/DataTableDemo/infinite/_components/live-row'
+import { RefreshButton } from 'components/interfaces/DataTableDemo/infinite/_components/refresh-button'
+import { getLevelRowClassName } from 'components/interfaces/DataTableDemo/lib/request/level'
+import { arrSome, inDateRange } from 'components/interfaces/DataTableDemo/lib/table/filterfns'
+
+// components pulled out and modified
+import { DataTableHeaderLayout } from '../data-table/data-table-header-layout'
+import { DataTableSideBarLayout } from '../data-table/data-table-side-bar-layout'
+import { FilterSideBar } from '../data-table/filter-side-bar'
+
+// specific imports
+import { useParams } from 'common'
+import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
+import {
+ ChartConfig,
+ cn,
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+ Separator,
+ Tabs_Shadcn_ as Tabs,
+ TabsContent_Shadcn_ as TabsContent,
+ TabsList_Shadcn_ as TabsList,
+ TabsTrigger_Shadcn_ as TabsTrigger,
+} from 'ui'
+
+import { columns, logEventBus } from './columns'
+import { MemoizedDataTableSheetContent } from './components/data-table-sheet-content'
+import { FunctionLogsTab } from './components/function-logs-tab'
+import { TraceDetailTab } from './components/trace-detail-tab'
+import { filterFields as defaultFilterFields, sheetFields } from './constants'
+import { dataOptions, useChartData } from './query-options-new'
+
+import { CHART_CONFIG, SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
+import { getFacetedUniqueValues, useLiveMode, useResetFocus } from './UnifiedLogs.utils'
+
+// Debug mode flag - set to true to enable detailed logs
+const DEBUG_FILTER_PROCESSING = false
+
+/**
+ * [Joshen] Slowly... This branch is a huge mess... Needs a shit ton of refactoring
+ * - Deprecate infinite-logs-example
+ * - Deprecate data-table
+ * - Deprecate DataTableDemo
+ * - Deprecate Trace (?)
+ * - Rename logs-new to UnifiedLogs
+ * - Components all need to be renamed for follow convention
+ */
+
+export const UnifiedLogsTable = () => {
+ useResetFocus()
+
+ const { ref: projectRef } = useParams()
+ const [search, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
+
+ const { sort, start, size, uuid, cursor, direction, live, ...filter } = search
+ const defaultColumnSorting = sort ? [sort] : []
+ const defaultColumnVisibility = { uuid: false }
+ const defaultRowSelection = search.uuid ? { [search.uuid]: true } : {}
+ const defaultColumnFilters = Object.entries(filter)
+ .map(([key, value]) => ({ id: key, value }))
+ .filter(({ value }) => value ?? undefined)
+
+ const [topBarHeight, setTopBarHeight] = useState(0)
+ const [activeTab, setActiveTab] = useState('details')
+ const [sorting, setSorting] = useState(defaultColumnSorting)
+ const [columnFilters, setColumnFilters] = useState(defaultColumnFilters)
+ const [rowSelection, setRowSelection] = useState(defaultRowSelection)
+
+ const [columnVisibility, setColumnVisibility] = useLocalStorageQuery(
+ 'data-table-visibility',
+ defaultColumnVisibility
+ )
+ const [columnOrder, setColumnOrder] = useLocalStorageQuery(
+ 'data-table-column-order',
+ []
+ )
+
+ const { data, isFetching, isLoading, fetchNextPage, hasNextPage, fetchPreviousPage, refetch } =
+ // @ts-ignore
+ useInfiniteQuery(dataOptions(search, projectRef ?? ''))
+
+ const flatData = useMemo(() => {
+ return data?.pages?.flatMap((page) => page.data ?? []) ?? []
+ }, [data?.pages])
+ const liveMode = useLiveMode(flatData)
+
+ // Add the chart data query for the entire time period
+ const { data: chartDataResult } = useChartData(search, projectRef ?? '')
+
+ // REMINDER: meta data is always the same for all pages as filters do not change(!)
+ const lastPage = data?.pages?.[data?.pages.length - 1]
+ // Use the totalCount from chartDataResult which gives us the actual count of logs in the time period
+ // instead of the hardcoded 10000 value
+ const totalDBRowCount = chartDataResult?.totalCount || lastPage?.meta?.totalRowCount
+ const filterDBRowCount = lastPage?.meta?.filterRowCount
+ const metadata = lastPage?.meta?.metadata
+ // Use chart data from the separate query if available, fallback to the default
+ const chartData = chartDataResult?.chartData || lastPage?.meta?.chartData
+ const facets = lastPage?.meta?.facets
+ const totalFetched = flatData?.length
+
+ // Create a filtered version of the chart config based on selected levels
+ const filteredChartConfig = useMemo(() => {
+ const levelFilter = search.level || ['success', 'warning', 'error']
+ return Object.fromEntries(
+ Object.entries(CHART_CONFIG).filter(([key]) => levelFilter.includes(key as any))
+ ) as ChartConfig
+ }, [search.level])
+
+ const getRowClassName = (
+ row: Row
+ ) => {
+ 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')
+ }
+
+ const table = useReactTable({
+ data: flatData,
+ columns,
+ state: {
+ columnFilters,
+ sorting,
+ columnVisibility,
+ rowSelection,
+ columnOrder,
+ },
+ enableMultiRowSelection: false,
+ columnResizeMode: 'onChange',
+ filterFns: { inDateRange, arrSome },
+ meta: { getRowClassName },
+ getRowId: (row) => row.uuid,
+ onColumnVisibilityChange: setColumnVisibility,
+ onColumnFiltersChange: setColumnFilters,
+ onRowSelectionChange: setRowSelection,
+ onSortingChange: setSorting,
+ onColumnOrderChange: setColumnOrder,
+ getSortedRowModel: getSortedRowModel(),
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getTTableFacetedUniqueValues(),
+ getFacetedMinMaxValues: getTTableFacetedMinMaxValues(),
+ // Here, manually override the filter function for the level column
+ // to prevent client-side filtering since it's already filtered on the server
+ // Always return true to pass all level values
+ // columnFilterFns: { level: () => true },
+ // debugAll: process.env.NEXT_PUBLIC_TABLE_DEBUG === 'true',
+ })
+
+ const selectedRow = useMemo(() => {
+ if ((isLoading || isFetching) && !flatData.length) return
+ const selectedRowKey = Object.keys(rowSelection)?.[0]
+ return table.getCoreRowModel().flatRows.find((row) => row.id === selectedRowKey)
+ }, [rowSelection, table, isLoading, isFetching, flatData])
+
+ const selectedRowKey = Object.keys(rowSelection)?.[0]
+
+ // REMINDER: this is currently needed for the cmdk search
+ // TODO: auto search via API when the user changes the filter instead of hardcoded
+ const filterFields = useMemo(() => {
+ return defaultFilterFields.map((field) => {
+ const facetsField = facets?.[field.value]
+ if (!facetsField) return field
+ if (field.options && field.options.length > 0) return field
+
+ // REMINDER: if no options are set, we need to set them via the API
+ const options = facetsField.rows.map(({ value }) => ({ label: `${value}`, value }))
+
+ if (field.type === ('slider' as any)) {
+ return {
+ ...(field as any),
+ min: facetsField.min ?? (field as any).min,
+ max: facetsField.max ?? (field as any).max,
+ options,
+ }
+ }
+
+ return { ...field, options }
+ })
+ }, [facets])
+
+ useEffect(() => {
+ if (DEBUG_FILTER_PROCESSING) console.log('========== FILTER CHANGE DETECTED ==========')
+ if (DEBUG_FILTER_PROCESSING) console.log('Raw columnFilters:', JSON.stringify(columnFilters))
+
+ // Check for level filters specifically
+ const levelColumnFilter = columnFilters.find((filter) => filter.id === 'level')
+ if (DEBUG_FILTER_PROCESSING) console.log('Level column filter:', levelColumnFilter)
+
+ const columnFiltersWithNullable = filterFields.map((field) => {
+ const filterValue = columnFilters.find((filter) => filter.id === field.value)
+ if (DEBUG_FILTER_PROCESSING) console.log(`Processing field ${field.value}:`, filterValue)
+ if (!filterValue) return { id: field.value, value: null }
+ return { id: field.value, value: filterValue.value }
+ })
+
+ // Debug level filter specifically
+ const levelFilter = columnFiltersWithNullable.find((f) => f.id === 'level')
+ if (DEBUG_FILTER_PROCESSING) console.log('Level filter after mapping:', levelFilter)
+
+ if (DEBUG_FILTER_PROCESSING)
+ console.log('All column filters after mapping:', columnFiltersWithNullable)
+
+ const search = columnFiltersWithNullable.reduce(
+ (prev, curr) => {
+ if (DEBUG_FILTER_PROCESSING)
+ console.log(`Processing filter for URL: ${curr.id}`, {
+ value: curr.value,
+ type: Array.isArray(curr.value) ? 'array' : typeof curr.value,
+ isEmpty: Array.isArray(curr.value) && curr.value.length === 0,
+ isNull: curr.value === null,
+ })
+
+ // Add to search parameters
+ prev[curr.id as string] = curr.value
+ return prev
+ },
+ {} as Record
+ )
+
+ if (DEBUG_FILTER_PROCESSING) console.log('Final search object to be set in URL:', search)
+ if (DEBUG_FILTER_PROCESSING) console.log('Level value in final search:', search.level)
+ if (DEBUG_FILTER_PROCESSING) console.log('Is level in search object:', 'level' in search)
+
+ // Set the search state without any console logs
+ if (DEBUG_FILTER_PROCESSING) console.log('CALLING setSearch with:', JSON.stringify(search))
+ setSearch(search)
+ if (DEBUG_FILTER_PROCESSING) console.log('========== END FILTER PROCESSING ==========')
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [columnFilters])
+
+ useEffect(() => {
+ setSearch({ sort: sorting?.[0] || null })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [sorting])
+
+ // TODO: can only share uuid within the first batch
+ useEffect(() => {
+ if (isLoading || isFetching) return
+ if (Object.keys(rowSelection)?.length && !selectedRow) {
+ setSearch({ uuid: null })
+ setRowSelection({})
+ } else {
+ setSearch({ uuid: Object.keys(rowSelection)?.[0] || null })
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [rowSelection, selectedRow, isLoading, isFetching])
+
+ // Set up event listener for trace tab selection
+ useEffect(() => {
+ const unsubscribe = logEventBus.on('selectTraceTab', (rowId) => {
+ setRowSelection({ [rowId]: true })
+ setActiveTab('trace')
+ })
+ return () => {
+ unsubscribe()
+ }
+ }, [setRowSelection])
+
+ return (
+
+
+
+
+
+
+ [
+ ,
+ fetchPreviousPage ? (
+
+ ) : null,
+ ]}
+ />
+
+
+
+
+
+
+
+
+
+
+ {
+ if (!liveMode.timestamp) return null
+ if (props?.row.original.uuid !== liveMode?.row?.uuid) return null
+ return
+ }}
+ setColumnOrder={setColumnOrder}
+ setColumnVisibility={setColumnVisibility}
+ />
+
+
+ {selectedRow?.original?.logs && selectedRow?.original?.logs?.length > 0 && (
+ <>
+
+
+
+
+
+ Function Logs (
+ {selectedRow?.original?.logs &&
+ Array.isArray(selectedRow?.original?.logs)
+ ? selectedRow?.original?.logs?.length
+ : 0}
+ )
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ {selectedRowKey && (
+ <>
+
+
+
+
+
+
+
+
+ Log Details
+ Trace
+
+
+
+
+
+
+
+ {selectedRow?.original?.has_trace ? (
+
+ ) : (
+
+
+ No trace found for this log
+
+
+ )}
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/logs-new/columns.tsx b/apps/studio/components/interfaces/UnifiedLogs/columns.tsx
similarity index 66%
rename from apps/studio/components/interfaces/logs-new/columns.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/columns.tsx
index 5134086a848..e54ceb2cc1d 100644
--- a/apps/studio/components/interfaces/logs-new/columns.tsx
+++ b/apps/studio/components/interfaces/UnifiedLogs/columns.tsx
@@ -1,35 +1,15 @@
-'use client'
+import type { ColumnDef } from '@tanstack/react-table'
+import { Bolt } from 'lucide-react'
import { TextWithTooltip } from 'components/interfaces/DataTableDemo/components/custom/text-with-tooltip'
import { DataTableColumnHeader } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column-header'
-import { DataTableColumnLatency } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-latency'
import { DataTableColumnLevelIndicator } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-level-indicator'
-import { DataTableColumnRegion } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-region'
import { DataTableColumnStatusCode } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-status-code'
-import {
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
- TooltipTrigger,
- Tooltip,
- TooltipContent,
-} from 'ui'
-import {
- getTimingColor,
- getTimingLabel,
- getTimingPercentage,
- timingPhases,
-} from 'components/interfaces/DataTableDemo/lib/request/timing'
-import { cn } from 'ui'
-import { HoverCardPortal } from '@radix-ui/react-hover-card'
-import type { ColumnDef, FilterFn } from '@tanstack/react-table'
-import { Bolt, Minus, Pyramid, User, Zap } from 'lucide-react'
import { HoverCardTimestamp } from 'components/interfaces/DataTableDemo/infinite/_components/hover-card-timestamp'
-
-// custom imports
-import type { ColumnSchema } from './schema'
-import { LogTypeIcon } from './LogTypeIcon'
+import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { AuthUserHoverCard } from './components/auth-user-hover-card'
+import { LogTypeIcon } from './LogTypeIcon'
+import type { ColumnSchema } from './UnifiedLogs.schema'
// Event broadcaster for selecting the trace tab
// This will be used to communicate between the columns and the client component
@@ -88,39 +68,6 @@ export const columns: ColumnDef[] = [
'font-mono w-[--col-date-size] max-w-[--col-date-size] min-w-[--col-date-size]',
},
},
- // {
- // accessorKey: 'id',
- // header: ({ column }) => ,
- // cell: ({ row }) => {
- // const value = row.getValue('id')
- // return
- // },
- // size: 130,
- // minSize: 130,
- // meta: {
- // headerClassName: 'w-[--header-id-size] max-w-[--header-id-size] min-w-[--header-id-size]',
- // cellClassName: 'w-[--col-id-size] max-w-[--col-id-size] min-w-[--col-id-size]',
- // },
- // },
- // {
- // id: 'uuid',
- // accessorKey: 'uuid',
- // header: 'Request Id',
- // cell: ({ row }) => {
- // const value = row.getValue('uuid')
- // return
- // },
- // enableResizing: false,
- // size: 130,
- // minSize: 130,
- // meta: {
- // label: 'Request Id',
- // cellClassName:
- // 'font-mono w-[--col-uuid-size] max-w-[--col-uuid-size] min-w-[--col-uuid-size]',
- // headerClassName:
- // 'min-w-[--header-uuid-size] w-[--header-uuid-size] max-w-[--header-uuid-size]',
- // },
- // },
{
accessorKey: 'log_type',
header: '',
@@ -238,57 +185,6 @@ export const columns: ColumnDef[] = [
headerClassName: 'min-w-[--header-pathname-size] w-[--header-pathname-size]',
},
},
- // {
- // accessorKey: 'latency',
- // // TODO: check if we can right align the table header/cell (makes is easier to read)
- // header: ({ column }) => ,
- // cell: ({ row }) => {
- // const value = row.getValue('latency')
- // return
- // },
- // filterFn: 'inNumberRange',
- // enableResizing: false,
- // size: 110,
- // minSize: 110,
- // meta: {
- // headerClassName:
- // 'w-[--header-latency-size] max-w-[--header-latency-size] min-w-[--header-latency-size]',
- // cellClassName:
- // 'font-mono w-[--col-latency-size] max-w-[--col-latency-size] min-w-[--col-latency-size]',
- // },
- // },
- // {
- // accessorKey: 'regions',
- // header: 'Region',
- // cell: ({ row }) => {
- // const value = row.getValue('regions')
- // if (Array.isArray(value)) {
- // if (value.length > 1) {
- // return {value.join(', ')}
- // } else {
- // return (
- //
- //
- //
- // )
- // }
- // }
- // if (typeof value === 'string') {
- // return
- // }
- // return
- // },
- // filterFn: 'arrIncludesSome',
- // enableResizing: false,
- // size: 163,
- // minSize: 163,
- // meta: {
- // headerClassName:
- // 'w-[--header-regions-size] max-w-[--header-regions-size] min-w-[--header-regions-size]',
- // cellClassName:
- // 'font-mono w-[--col-regions-size] max-w-[--col-regions-size] min-w-[--col-regions-size]',
- // },
- // },
{
accessorKey: 'event_message',
header: ({ column }) => ,
@@ -318,10 +214,9 @@ export const columns: ColumnDef[] = [
)
},
- enableResizing: true, // allow manual resizing
+ enableResizing: true,
size: 400,
minSize: 400,
- // ❌ don't set maxSize — let it grow
meta: {
headerClassName:
'w-[--header-event_message-size] max-w-[--header-event_message-size] min-w-[--header-event_message-size]',
diff --git a/apps/studio/components/interfaces/logs-new/components/auth-user-hover-card.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/auth-user-hover-card.tsx
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/components/auth-user-hover-card.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/components/auth-user-hover-card.tsx
diff --git a/apps/studio/components/interfaces/logs-new/components/data-table-sheet-content.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/data-table-sheet-content.tsx
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/components/data-table-sheet-content.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/components/data-table-sheet-content.tsx
diff --git a/apps/studio/components/interfaces/logs-new/components/data-table-sheet-details-layout.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/data-table-sheet-details-layout.tsx
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/components/data-table-sheet-details-layout.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/components/data-table-sheet-details-layout.tsx
diff --git a/apps/studio/components/interfaces/logs-new/components/function-logs-tab.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/function-logs-tab.tsx
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/components/function-logs-tab.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/components/function-logs-tab.tsx
diff --git a/apps/studio/components/interfaces/logs-new/components/tabbed-detail-panel.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/tabbed-detail-panel.tsx
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/components/tabbed-detail-panel.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/components/tabbed-detail-panel.tsx
diff --git a/apps/studio/components/interfaces/logs-new/components/trace-button.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/trace-button.tsx
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/components/trace-button.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/components/trace-button.tsx
diff --git a/apps/studio/components/interfaces/logs-new/components/trace-detail-tab.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/trace-detail-tab.tsx
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/components/trace-detail-tab.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/components/trace-detail-tab.tsx
diff --git a/apps/studio/components/interfaces/logs-new/constants.tsx b/apps/studio/components/interfaces/UnifiedLogs/constants.tsx
similarity index 90%
rename from apps/studio/components/interfaces/logs-new/constants.tsx
rename to apps/studio/components/interfaces/UnifiedLogs/constants.tsx
index ebed8cf5599..e97b3a6abe9 100644
--- a/apps/studio/components/interfaces/logs-new/constants.tsx
+++ b/apps/studio/components/interfaces/UnifiedLogs/constants.tsx
@@ -1,8 +1,6 @@
-'use client'
+import { format } from 'date-fns'
+import { User } from 'lucide-react'
-import { CopyToClipboardContainer } from 'components/interfaces/DataTableDemo/components/custom/copy-to-clipboard-container'
-import { KVTabs } from 'components/interfaces/DataTableDemo/components/custom/kv-tabs'
-import { DataTableColumnRegion } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-region'
import type {
DataTableFilterField,
Option,
@@ -10,20 +8,14 @@ import type {
} from 'components/interfaces/DataTableDemo/components/data-table/types'
import { LEVELS } from 'components/interfaces/DataTableDemo/constants/levels'
import { METHODS } from 'components/interfaces/DataTableDemo/constants/method'
-import { REGIONS } from 'components/interfaces/DataTableDemo/constants/region'
-import { formatMilliseconds } from 'components/interfaces/DataTableDemo/lib/format'
import { getLevelColor, getLevelLabel } from 'components/interfaces/DataTableDemo/lib/request/level'
import { getStatusColor } from 'components/interfaces/DataTableDemo/lib/request/status-code'
import { cn } from 'ui'
-import { format } from 'date-fns'
-import { PopoverPercentile } from 'components/interfaces/DataTableDemo/infinite/_components/popover-percentile'
-import { SheetTimingPhases } from 'components/interfaces/DataTableDemo/infinite/_components/sheet-timing-phases'
-import { User } from 'lucide-react'
// custom imports
-import type { LogsMeta } from './query-options'
-import { type ColumnSchema, LOG_TYPES } from './schema'
import { TraceButton } from './components/trace-button'
+import type { LogsMeta } from './query-options'
+import { type ColumnSchema, LOG_TYPES } from './UnifiedLogs.schema'
// instead of filterFields, maybe just 'fields' with a filterDisabled prop?
// that way, we could have 'message' or 'headers' field with label and value as well as type!
diff --git a/apps/studio/components/interfaces/logs-new/constants/date-preset.ts b/apps/studio/components/interfaces/UnifiedLogs/constants/date-preset.ts
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/constants/date-preset.ts
rename to apps/studio/components/interfaces/UnifiedLogs/constants/date-preset.ts
diff --git a/apps/studio/components/interfaces/logs-new/constants/levels.ts b/apps/studio/components/interfaces/UnifiedLogs/constants/levels.ts
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/constants/levels.ts
rename to apps/studio/components/interfaces/UnifiedLogs/constants/levels.ts
diff --git a/apps/studio/components/interfaces/logs-new/constants/method.ts b/apps/studio/components/interfaces/UnifiedLogs/constants/method.ts
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/constants/method.ts
rename to apps/studio/components/interfaces/UnifiedLogs/constants/method.ts
diff --git a/apps/studio/components/interfaces/logs-new/constants/query-params.ts b/apps/studio/components/interfaces/UnifiedLogs/constants/query-params.ts
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/constants/query-params.ts
rename to apps/studio/components/interfaces/UnifiedLogs/constants/query-params.ts
diff --git a/apps/studio/components/interfaces/logs-new/constants/region.ts b/apps/studio/components/interfaces/UnifiedLogs/constants/region.ts
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/constants/region.ts
rename to apps/studio/components/interfaces/UnifiedLogs/constants/region.ts
diff --git a/apps/studio/components/interfaces/logs-new/constants/tag.ts b/apps/studio/components/interfaces/UnifiedLogs/constants/tag.ts
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/constants/tag.ts
rename to apps/studio/components/interfaces/UnifiedLogs/constants/tag.ts
diff --git a/apps/studio/components/interfaces/logs-new/hooks/useUnifiedLogs.ts b/apps/studio/components/interfaces/UnifiedLogs/hooks/useUnifiedLogs.ts
similarity index 100%
rename from apps/studio/components/interfaces/logs-new/hooks/useUnifiedLogs.ts
rename to apps/studio/components/interfaces/UnifiedLogs/hooks/useUnifiedLogs.ts
diff --git a/apps/studio/components/interfaces/logs-new/logs.queries.ts b/apps/studio/components/interfaces/UnifiedLogs/logs.queries.ts
similarity index 99%
rename from apps/studio/components/interfaces/logs-new/logs.queries.ts
rename to apps/studio/components/interfaces/UnifiedLogs/logs.queries.ts
index 139783438fa..5f397d8cffc 100644
--- a/apps/studio/components/interfaces/logs-new/logs.queries.ts
+++ b/apps/studio/components/interfaces/UnifiedLogs/logs.queries.ts
@@ -4,7 +4,7 @@
import dayjs from 'dayjs'
import { BASE_CONDITIONS_EXCLUDED_PARAMS, EXCLUDED_QUERY_PARAMS } from './constants/query-params'
-import { SearchParamsType } from './search-params'
+import { SearchParamsType } from './UnifiedLogs.types'
// Types for plan IDs - import actual type if available
type PlanId = 'free' | 'pro' | 'team' | 'enterprise'
diff --git a/apps/studio/components/interfaces/logs-new/query-options-new.ts b/apps/studio/components/interfaces/UnifiedLogs/query-options-new.ts
similarity index 96%
rename from apps/studio/components/interfaces/logs-new/query-options-new.ts
rename to apps/studio/components/interfaces/UnifiedLogs/query-options-new.ts
index 1efbe48d70e..9f739500217 100644
--- a/apps/studio/components/interfaces/logs-new/query-options-new.ts
+++ b/apps/studio/components/interfaces/UnifiedLogs/query-options-new.ts
@@ -1,11 +1,11 @@
import { useQuery } from '@tanstack/react-query'
-import { ARRAY_DELIMITER, SORT_DELIMITER } from 'components/interfaces/DataTableDemo/lib/delimiters'
-import { post } from 'data/fetchers'
import dayjs from 'dayjs'
+
+import { post } from 'data/fetchers'
import { getLogsChartQuery, getLogsCountQuery, getUnifiedLogsQuery } from './logs.queries'
-import type { BaseChartSchema, FacetMetadataSchema } from './schema'
-import { ColumnSchema } from './schema'
-import type { SearchParamsType } from './search-params'
+import { ARRAY_DELIMITER, SORT_DELIMITER } from './UnifiedLogs.constants'
+import { ColumnSchema, type BaseChartSchema, type FacetMetadataSchema } from './UnifiedLogs.schema'
+import type { SearchParamsType, UnifiedLogsMeta } from './UnifiedLogs.types'
// Debug mode flag - set to true to enable detailed logs
const DEBUG_MODE = false
@@ -23,11 +23,6 @@ export type UnifiedLogSchema = {
auth_user: string | null
}
-export type UnifiedLogsMeta = {
- logTypeCounts: Record
- currentPercentiles: Record
-}
-
// Extended column schema to include raw timestamp
export type ExtendedColumnSchema = ColumnSchema & {
timestamp: string // Original database timestamp
@@ -279,15 +274,15 @@ export const dataOptions = (search: SearchParamsType, projectRef: string) => {
)
// Calculate the appropriate initial cursor based on the selected date range
- const getInitialCursor = () => {
- if (search.date && search.date.length === 2) {
- // Use the end of the selected date range
- return new Date(search.date[1]).getTime()
- } else {
- // Default to current time if no date range is selected
- return new Date().getTime()
- }
- }
+ // const getInitialCursor = () => {
+ // if (search.date && search.date.length === 2) {
+ // // Use the end of the selected date range
+ // return new Date(search.date[1]).getTime()
+ // } else {
+ // // Default to current time if no date range is selected
+ // return new Date().getTime()
+ // }
+ // }
// Simply return the options object
return {
diff --git a/apps/studio/components/interfaces/logs-new/query-options.ts b/apps/studio/components/interfaces/UnifiedLogs/query-options.ts
similarity index 96%
rename from apps/studio/components/interfaces/logs-new/query-options.ts
rename to apps/studio/components/interfaces/UnifiedLogs/query-options.ts
index 1ad15cfaf0a..f0e8bc20cd6 100644
--- a/apps/studio/components/interfaces/logs-new/query-options.ts
+++ b/apps/studio/components/interfaces/UnifiedLogs/query-options.ts
@@ -2,15 +2,10 @@ import type { Percentile } from 'components/interfaces/DataTableDemo/lib/request
// Removed keepPreviousData import as it causes persistent lint errors
// import { keepPreviousData } from '@tanstack/react-query'
import SuperJSON from 'superjson'
-import type { BaseChartSchema, ColumnSchema, FacetMetadataSchema } from './schema'
-import type { SearchParamsType } from './search-params'
+import type { BaseChartSchema, ColumnSchema, FacetMetadataSchema } from './UnifiedLogs.schema'
// Removed: import { searchParamsSerializer } from './search-params'
-import {
- ARRAY_DELIMITER,
- RANGE_DELIMITER,
- SLIDER_DELIMITER,
- SORT_DELIMITER,
-} from 'components/interfaces/DataTableDemo/lib/delimiters'
+import { ARRAY_DELIMITER, SORT_DELIMITER } from 'components/interfaces/DataTableDemo/lib/delimiters'
+import { SearchParamsType } from './UnifiedLogs.types'
// Restore the helper function to create query params string
const createApiQueryString = (params: Record): string => {
diff --git a/apps/studio/components/interfaces/logs-new/client.tsx b/apps/studio/components/interfaces/logs-new/client.tsx
deleted file mode 100644
index da438eba99f..00000000000
--- a/apps/studio/components/interfaces/logs-new/client.tsx
+++ /dev/null
@@ -1,552 +0,0 @@
-'use client'
-
-import { useQueryState, useQueryStates } from 'nuqs'
-import * as React from 'react'
-
-import { useInfiniteQuery } from '@tanstack/react-query'
-import {
- ColumnFiltersState,
- getCoreRowModel,
- getFacetedRowModel,
- getFilteredRowModel,
- getSortedRowModel,
- getFacetedMinMaxValues as getTTableFacetedMinMaxValues,
- getFacetedUniqueValues as getTTableFacetedUniqueValues,
- Row,
- RowSelectionState,
- SortingState,
- useReactTable,
- VisibilityState,
- type Table as TTable,
-} from '@tanstack/react-table'
-import { DataTableInfinite } from 'components/interfaces/data-table/data-table-infinite'
-import { TimelineChart } from 'components/interfaces/data-table/timeline-chart'
-import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
-import { LiveRow } from 'components/interfaces/DataTableDemo/infinite/_components/live-row'
-
-import {
- getLevelLabel,
- getLevelRowClassName,
-} from 'components/interfaces/DataTableDemo/lib/request/level'
-import { arrSome, inDateRange } from 'components/interfaces/DataTableDemo/lib/table/filterfns'
-import { ChartConfig, cn, Separator } from 'ui'
-import { DataTableFilterCommand } from 'components/interfaces/DataTableDemo/components/data-table/data-table-filter-command'
-import {
- DataTableProvider,
- useDataTable,
-} from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
-import { DataTableSheetDetails } from 'components/interfaces/DataTableDemo/components/data-table/data-table-sheet/data-table-sheet-details'
-import { DataTableToolbar } from 'components/interfaces/DataTableDemo/components/data-table/data-table-toolbar'
-import { useLocalStorage } from 'components/interfaces/DataTableDemo/hooks/use-local-storage'
-import { LiveButton } from 'components/interfaces/DataTableDemo/infinite/_components/live-button'
-import { RefreshButton } from 'components/interfaces/DataTableDemo/infinite/_components/refresh-button'
-import { Percentile } from 'components/interfaces/DataTableDemo/lib/request/percentile'
-import {
- ResizableHandle,
- ResizablePanel,
- ResizablePanelGroup,
-} from 'ui/src/components/shadcn/ui/resizable'
-import { Button } from 'ui'
-import { X } from 'lucide-react'
-
-// components pulled out and modified
-import { FilterSideBar } from '../data-table/filter-side-bar'
-import { DataTableHeaderLayout } from '../data-table/data-table-header-layout'
-import { DataTableSideBarLayout } from '../data-table/data-table-side-bar-layout'
-
-// specific imports
-import { MemoizedDataTableSheetContent } from './components/data-table-sheet-content'
-import type { FacetMetadataSchema, TimelineChartSchema } from './schema'
-import { columns, logEventBus } from './columns'
-import { searchParamsParser } from './search-params'
-import { dataOptions, useChartData } from './query-options-new'
-import { filterFields as defaultFilterFields, sheetFields } from './constants'
-import { useParams } from 'common'
-import { TraceDetailTab } from './components/trace-detail-tab'
-import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ui/src/components/shadcn/ui/tabs'
-import { FunctionLogsTab } from './components/function-logs-tab'
-
-// Debug mode flag - set to true to enable detailed logs
-const DEBUG_FILTER_PROCESSING = false
-
-function TooltipLabel({ level }: { level: keyof Omit }) {
- return (
-
-
{level}
-
{getLevelLabel(level)}
-
- )
-}
-
-const chartConfig = {
- success: {
- label: ,
- color: 'hsl(var(--foreground-muted))',
- },
- warning: {
- label: ,
- color: 'hsl(var(--warning-default))',
- },
- error: {
- label: ,
- color: 'hsl(var(--destructive-default))',
- },
-} satisfies ChartConfig
-
-export function Client() {
- const [search, setSearch] = useQueryStates(searchParamsParser)
- const { ref: projectRef } = useParams()
- const [activeTab, setActiveTab] = React.useState('details')
-
- const { data, isFetching, isLoading, fetchNextPage, hasNextPage, fetchPreviousPage, refetch } =
- useInfiniteQuery(dataOptions(search, projectRef ?? ''))
-
- // Add the chart data query for the entire time period
- const { data: chartDataResult, isLoading: isChartLoading } = useChartData(
- search,
- projectRef ?? ''
- )
-
- useResetFocus()
-
- const [topBarHeight, setTopBarHeight] = React.useState(0)
-
- const flatData = React.useMemo(() => {
- return data?.pages?.flatMap((page) => page.data ?? []) ?? []
- }, [data?.pages])
-
- const liveMode = useLiveMode(flatData)
-
- // REMINDER: meta data is always the same for all pages as filters do not change(!)
- const lastPage = data?.pages?.[data?.pages.length - 1]
- // Use the totalCount from chartDataResult which gives us the actual count of logs in the time period
- // instead of the hardcoded 10000 value
- const totalDBRowCount = chartDataResult?.totalCount || lastPage?.meta?.totalRowCount
- const filterDBRowCount = lastPage?.meta?.filterRowCount
- const metadata = lastPage?.meta?.metadata
- // Use chart data from the separate query if available, fallback to the default
- const chartData = chartDataResult?.chartData || lastPage?.meta?.chartData
- const facets = lastPage?.meta?.facets
- const totalFetched = flatData?.length
-
- const { sort, start, size, uuid, cursor, direction, live, ...filter } = search
-
- // Create a filtered version of the chart config based on selected levels
- const filteredChartConfig = React.useMemo(() => {
- const levelFilter = search.level || ['success', 'warning', 'error']
- return Object.fromEntries(
- Object.entries(chartConfig).filter(([key]) => levelFilter.includes(key))
- ) as ChartConfig
- }, [search.level])
-
- const defaultColumnFilters = Object.entries(filter)
- .map(([key, value]) => ({
- id: key,
- value,
- }))
- .filter(({ value }) => value ?? undefined)
-
- const defaultColumnSorting = sort ? [sort] : []
- const defaultColumnVisibility = {
- uuid: false,
- }
- const defaultRowSelection = search.uuid ? { [search.uuid]: true } : {}
-
- const [columnFilters, setColumnFilters] = React.useState(defaultColumnFilters)
- const [sorting, setSorting] = React.useState(defaultColumnSorting)
- const [columnVisibility, setColumnVisibility] = useLocalStorage(
- 'data-table-visibility',
- defaultColumnVisibility
- )
- const [rowSelection, setRowSelection] = React.useState(defaultRowSelection)
- const [columnOrder, setColumnOrder] = useLocalStorage('data-table-column-order', [])
-
- // REMINDER: this is currently needed for the cmdk search
- // TODO: auto search via API when the user changes the filter instead of hardcoded
- const filterFields = React.useMemo(() => {
- return defaultFilterFields.map((field) => {
- const facetsField = facets?.[field.value]
- if (!facetsField) return field
- if (field.options && field.options.length > 0) return field
-
- // REMINDER: if no options are set, we need to set them via the API
- const options = facetsField.rows.map(({ value }) => {
- return {
- label: `${value}`,
- value,
- }
- })
-
- if (field.type === ('slider' as any)) {
- return {
- ...(field as any),
- min: facetsField.min ?? (field as any).min,
- max: facetsField.max ?? (field as any).max,
- options,
- }
- }
-
- return { ...field, options }
- })
- }, [facets])
-
- const getRowClassName = (
- row: Row
- ) => {
- 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')
- }
-
- const table = useReactTable({
- data: flatData,
- columns,
- state: {
- columnFilters,
- sorting,
- columnVisibility,
- rowSelection,
- columnOrder,
- },
- enableMultiRowSelection: false,
- columnResizeMode: 'onChange',
- getRowId: (row) => row.uuid,
- onColumnVisibilityChange: setColumnVisibility,
- onColumnFiltersChange: setColumnFilters,
- onRowSelectionChange: setRowSelection,
- onSortingChange: setSorting,
- onColumnOrderChange: setColumnOrder,
- getSortedRowModel: getSortedRowModel(),
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getFacetedRowModel: getFacetedRowModel(),
- getFacetedUniqueValues: getTTableFacetedUniqueValues(),
- getFacetedMinMaxValues: getTTableFacetedMinMaxValues(),
- filterFns: { inDateRange, arrSome },
- // Here, manually override the filter function for the level column
- // to prevent client-side filtering since it's already filtered on the server
- columnFilterFns: {
- level: () => true, // Always return true to pass all level values
- },
- // debugAll: process.env.NEXT_PUBLIC_TABLE_DEBUG === 'true',
- meta: { getRowClassName },
- })
-
- const selectedRow = React.useMemo(() => {
- if ((isLoading || isFetching) && !flatData.length) return
- const selectedRowKey = Object.keys(rowSelection)?.[0]
- return table.getCoreRowModel().flatRows.find((row) => row.id === selectedRowKey)
- }, [rowSelection, table, isLoading, isFetching, flatData])
-
- const selectedRowKey = Object.keys(rowSelection)?.[0]
-
- React.useEffect(() => {
- if (DEBUG_FILTER_PROCESSING) console.log('========== FILTER CHANGE DETECTED ==========')
- if (DEBUG_FILTER_PROCESSING) console.log('Raw columnFilters:', JSON.stringify(columnFilters))
-
- // Check for level filters specifically
- const levelColumnFilter = columnFilters.find((filter) => filter.id === 'level')
- if (DEBUG_FILTER_PROCESSING) console.log('Level column filter:', levelColumnFilter)
-
- const columnFiltersWithNullable = filterFields.map((field) => {
- const filterValue = columnFilters.find((filter) => filter.id === field.value)
- if (DEBUG_FILTER_PROCESSING) console.log(`Processing field ${field.value}:`, filterValue)
- if (!filterValue) return { id: field.value, value: null }
- return { id: field.value, value: filterValue.value }
- })
-
- // Debug level filter specifically
- const levelFilter = columnFiltersWithNullable.find((f) => f.id === 'level')
- if (DEBUG_FILTER_PROCESSING) console.log('Level filter after mapping:', levelFilter)
-
- if (DEBUG_FILTER_PROCESSING)
- console.log('All column filters after mapping:', columnFiltersWithNullable)
-
- const search = columnFiltersWithNullable.reduce(
- (prev, curr) => {
- if (DEBUG_FILTER_PROCESSING)
- console.log(`Processing filter for URL: ${curr.id}`, {
- value: curr.value,
- type: Array.isArray(curr.value) ? 'array' : typeof curr.value,
- isEmpty: Array.isArray(curr.value) && curr.value.length === 0,
- isNull: curr.value === null,
- })
-
- // Add to search parameters
- prev[curr.id as string] = curr.value
- return prev
- },
- {} as Record
- )
-
- if (DEBUG_FILTER_PROCESSING) console.log('Final search object to be set in URL:', search)
- if (DEBUG_FILTER_PROCESSING) console.log('Level value in final search:', search.level)
- if (DEBUG_FILTER_PROCESSING) console.log('Is level in search object:', 'level' in search)
-
- // Set the search state without any console logs
- if (DEBUG_FILTER_PROCESSING) console.log('CALLING setSearch with:', JSON.stringify(search))
- setSearch(search)
- if (DEBUG_FILTER_PROCESSING) console.log('========== END FILTER PROCESSING ==========')
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [columnFilters])
-
- React.useEffect(() => {
- setSearch({ sort: sorting?.[0] || null })
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [sorting])
-
- // TODO: can only share uuid within the first batch
- React.useEffect(() => {
- if (isLoading || isFetching) return
- if (Object.keys(rowSelection)?.length && !selectedRow) {
- setSearch({ uuid: null })
- setRowSelection({})
- } else {
- setSearch({ uuid: Object.keys(rowSelection)?.[0] || null })
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [rowSelection, selectedRow, isLoading, isFetching])
-
- // Set up event listener for trace tab selection
- React.useEffect(() => {
- const unsubscribe = logEventBus.on('selectTraceTab', (rowId) => {
- // Select the row
- setRowSelection({ [rowId]: true })
- // Set the active tab to trace
- setActiveTab('trace')
- })
-
- return () => {
- unsubscribe()
- }
- }, [setRowSelection])
-
- return (
- <>
-
-
-
-
-
-
- [
- ,
- fetchPreviousPage ? (
-
- ) : null,
- ]}
- />
-
-
-
- {/* Use ResizablePanelGroup for the log list and details */}
- {/*
*/}
-
-
-
-
-
- {
- if (!liveMode.timestamp) return null
- if (props?.row.original.uuid !== liveMode?.row?.uuid) return null
- return
- }}
- setColumnOrder={setColumnOrder}
- setColumnVisibility={setColumnVisibility}
- />
-
-
- {/*
-
- hello world
-
- {JSON.stringify(selectedRow?.original, null, 2)}
-
- */}
- {selectedRow?.original?.logs && selectedRow?.original?.logs?.length > 0 && (
- <>
-
-
-
-
-
- Function Logs (
- {selectedRow?.original?.logs &&
- Array.isArray(selectedRow?.original?.logs)
- ? selectedRow?.original?.logs?.length
- : 0}
- )
-
-
-
-
-
-
-
- >
- )}
-
-
-
- {selectedRowKey && (
- <>
-
-
-
-
-
-
-
-
- Log Details
- Trace
-
-
-
-
-
-
-
- {selectedRow?.original?.has_trace ? (
-
- ) : (
-
-
- No trace found for this log
-
-
- )}
-
-
-
-
-
-
-
- >
- )}
-
-
- {/*
*/}
-
-
- >
- )
-}
-
-function useResetFocus() {
- useHotKey(() => {
- // FIXME: some dedicated div[tabindex="0"] do not auto-unblur (e.g. the DataTableFilterResetButton)
- // REMINDER: we cannot just document.activeElement?.blur(); as the next tab will focus the next element in line,
- // which is not what we want. We want to reset entirely.
- document.body.setAttribute('tabindex', '0')
- document.body.focus()
- document.body.removeAttribute('tabindex')
- }, '.')
-}
-
-// TODO: make a BaseObject (incl. date and uuid e.g. for every upcoming branch of infinite table)
-export function useLiveMode(data: TData[]) {
- const [live] = useQueryState('live', searchParamsParser.live)
- // REMINDER: used to capture the live mode on timestamp
- const liveTimestamp = React.useRef(live ? new Date().getTime() : undefined)
-
- React.useEffect(() => {
- if (live) liveTimestamp.current = new Date().getTime()
- else liveTimestamp.current = undefined
- }, [live])
-
- const anchorRow = React.useMemo(() => {
- if (!live) return undefined
-
- const item = data.find((item) => {
- // return first item that is there if not liveTimestamp
- if (!liveTimestamp.current) return true
- // return first item that is after the liveTimestamp
- if (item.date.getTime() > liveTimestamp.current) return false
- return true
- // return first item if no liveTimestamp
- })
-
- return item
- }, [live, data])
-
- return { row: anchorRow, timestamp: liveTimestamp.current }
-}
-
-export function getFacetedUniqueValues(facets?: Record) {
- return (table: TTable, columnId: string) => {
- return new Map(facets?.[columnId]?.rows?.map(({ value, total }) => [value, total]) || [])
- }
-}
-
-export function getFacetedMinMaxValues(facets?: Record) {
- return (table: TTable, columnId: string) => {
- const min = facets?.[columnId]?.min
- const max = facets?.[columnId]?.max
- if (typeof min === 'number' && typeof max === 'number') return [min, max]
- if (typeof min === 'number') return [min, min]
- if (typeof max === 'number') return [max, max]
- return undefined
- }
-}
diff --git a/apps/studio/components/interfaces/logs-new/logs.utils.ts b/apps/studio/components/interfaces/logs-new/logs.utils.ts
deleted file mode 100644
index 9b82290d74d..00000000000
--- a/apps/studio/components/interfaces/logs-new/logs.utils.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Utilities for log handling and display
- */
-
-/**
- * Determines the log level based on status code
- * @param statusCode HTTP status code as number or string
- * @returns 'success', 'info', 'warn', or 'error'
- */
-export function determineLogLevelFromStatus(statusCode: string | number | undefined): string {
- // Default to info if no status code
- if (!statusCode) return 'info'
-
- // Convert to number if it's a string
- const code = typeof statusCode === 'string' ? parseInt(statusCode, 10) : statusCode
-
- // Handle NaN case
- if (isNaN(code)) return 'info'
-
- // Determine level based on status code ranges
- if (code >= 200 && code < 300) return 'success'
- if (code >= 400 && code < 500) return 'warning'
- if (code >= 500) return 'error'
-
- // Default for other codes
- return 'info'
-}
-
-/**
- * Determines the log level based on log type and status
- * Different log sources may have different level determination logic
- */
-export function determineLogLevel(
- logType: string,
- statusCode: string | number | undefined,
- defaultLevel?: string
-): string {
- // If a meaningful level is already provided, use it
- if (defaultLevel && defaultLevel !== 'undefined') return defaultLevel
-
- // Handle specific log types
- switch (logType) {
- case 'edge':
- return determineLogLevelFromStatus(statusCode)
- case 'auth':
- case 'edge function':
- return determineLogLevelFromStatus(statusCode)
-
- case 'postgres':
- // Could add postgres-specific error code handling here
- return statusCode ? 'warn' : 'info'
-
- case 'function events':
- // Default to info if not specified
- return defaultLevel || 'info'
-
- // Add more log types as needed
-
- default:
- return 'info'
- }
-}
diff --git a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx
index 99a7926186a..74b943fdf05 100644
--- a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx
+++ b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx
@@ -2,13 +2,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
+import { LOCAL_STORAGE_KEYS } from 'common'
import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { LogsSidebarMenuV2 } from './LogsSidebarMenuV2'
-import { LOCAL_STORAGE_KEYS } from 'common'
interface LogsLayoutProps {
title?: string
@@ -20,16 +20,15 @@ const LogsLayout = ({
children,
hideSidebar = false,
}: PropsWithChildren) => {
- const { isLoading, can: canUseLogsExplorer } = useAsyncCheckProjectPermissions(
- PermissionAction.ANALYTICS_READ,
- 'logflare'
- )
-
const router = useRouter()
const [_, setLastLogsPage] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_LOGS_PAGE,
router.pathname.split('/logs/')[1] || ''
)
+ const { isLoading, can: canUseLogsExplorer } = useAsyncCheckProjectPermissions(
+ PermissionAction.ANALYTICS_READ,
+ 'logflare'
+ )
useEffect(() => {
if (router.pathname.includes('/logs/')) {
diff --git a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx
index 993484f7197..f47831e49e3 100644
--- a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx
+++ b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx
@@ -80,22 +80,27 @@ export function SidebarCollapsible({
}
export function LogsSidebarMenuV2() {
+ const router = useRouter()
+ const { ref } = useParams() as { ref: string }
+ const warehouseEnabled = useFlag('warehouse')
+
const [searchText, setSearchText] = useState('')
const [createCollectionOpen, setCreateCollectionOpen] = useState(false)
const canCreateCollection = useCheckPermissions(PermissionAction.ANALYTICS_WRITE, 'logflare')
- const router = useRouter()
- const { ref } = useParams() as { ref: string }
+
const { data: tenantData } = useWarehouseTenantQuery({ projectRef: ref })
+
const {
projectAuthAll: authEnabled,
projectStorageAll: storageEnabled,
realtimeAll: realtimeEnabled,
} = useIsFeatureEnabled(['project_storage:all', 'project_auth:all', 'realtime:all'])
- const warehouseEnabled = useFlag('warehouse')
+
const { data: whCollections, isLoading: whCollectionsLoading } = useWarehouseCollectionsQuery(
{ projectRef: ref },
{ enabled: IS_PLATFORM && warehouseEnabled && !!tenantData }
)
+
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
index 42b55f8b983..83f7788a2ac 100644
--- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx
@@ -155,12 +155,6 @@ export const generateOtherRoutes = (ref?: string, project?: Project): Route[] =>
icon: ,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/unified-logs`),
},
- {
- key: 'unified-log-2',
- label: 'Unified Logs Open Status',
- icon: ,
- link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/logs-new`),
- },
{
key: 'api',
label: 'API Docs',
diff --git a/apps/studio/hooks/ui/useHotKey.ts b/apps/studio/hooks/ui/useHotKey.ts
new file mode 100644
index 00000000000..d0c381b9beb
--- /dev/null
+++ b/apps/studio/hooks/ui/useHotKey.ts
@@ -0,0 +1,17 @@
+import { useEffect } from 'react'
+
+export function useHotKey(callback: () => void, key: string): void {
+ useEffect(() => {
+ function handler(e: KeyboardEvent) {
+ if (e.key === key && (e.metaKey || e.ctrlKey)) {
+ // e.preventDefault();
+ callback()
+ }
+ }
+
+ window.addEventListener('keydown', handler)
+ return () => {
+ window.removeEventListener('keydown', handler)
+ }
+ }, [key])
+}
diff --git a/apps/studio/pages/project/[ref]/demo-infinite.tsx b/apps/studio/pages/project/[ref]/demo-infinite.tsx
deleted file mode 100644
index d32013d4fc8..00000000000
--- a/apps/studio/pages/project/[ref]/demo-infinite.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
-// import { SearchParams } from 'nuqs' // No longer needed directly
-import * as React from 'react'
-import { Client } from 'components/interfaces/DataTableDemo/infinite/client'
-import { dataOptions } from 'components/interfaces/DataTableDemo/light/query-options'
-import { searchParamsParser } from 'components/interfaces/DataTableDemo/light/search-params' // Import the parser
-// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
-import { useQueryStates } from 'nuqs' // Import useQueryStates
-
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
-
-import type { NextPageWithLayout } from 'types'
-
-export const LogPage: NextPageWithLayout = () => {
- // const searchParams = useSearchParams() // Replaced
- // const params = Object.fromEntries(searchParams.entries()) // Replaced
- // const search = searchParamsCache.parse(params) // Replaced
- const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
- const queryClient = getQueryClient()
-
- React.useEffect(() => {
- // Pass the parsed search object directly to dataOptions
- queryClient.prefetchInfiniteQuery(dataOptions(search))
- }, [search, queryClient]) // Add queryClient to dependencies
-
- return
-}
-
-LogPage.getLayout = (page) => (
-
- {page}
-
-)
-
-export default LogPage
diff --git a/apps/studio/pages/project/[ref]/demo-light.tsx b/apps/studio/pages/project/[ref]/demo-light.tsx
deleted file mode 100644
index 186251ae25f..00000000000
--- a/apps/studio/pages/project/[ref]/demo-light.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
-// import { SearchParams } from 'nuqs' // No longer needed directly
-import * as React from 'react'
-import { Client } from 'components/interfaces/DataTableDemo/light/client'
-import { dataOptions } from 'components/interfaces/DataTableDemo/light/query-options'
-import { searchParamsParser } from 'components/interfaces/DataTableDemo/light/search-params' // Import the parser
-// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
-import { useQueryStates } from 'nuqs' // Import useQueryStates
-
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
-
-import type { NextPageWithLayout } from 'types'
-
-export const LogPage: NextPageWithLayout = () => {
- // const searchParams = useSearchParams() // Replaced
- // const params = Object.fromEntries(searchParams.entries()) // Replaced
- // const search = searchParamsCache.parse(params) // Replaced
- const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
- const queryClient = getQueryClient()
-
- React.useEffect(() => {
- // Pass the parsed search object directly to dataOptions
- queryClient.prefetchInfiniteQuery(dataOptions(search))
- }, [search, queryClient]) // Add queryClient to dependencies
-
- return
-}
-
-LogPage.getLayout = (page) => (
-
- {page}
-
-)
-
-export default LogPage
diff --git a/apps/studio/pages/project/[ref]/demo.tsx b/apps/studio/pages/project/[ref]/demo.tsx
deleted file mode 100644
index d4b38a66f2d..00000000000
--- a/apps/studio/pages/project/[ref]/demo.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
-import * as React from 'react'
-import type { NextPageWithLayout } from 'types'
-import { columns } from 'components/interfaces/DataTableDemo/default/columns'
-import { filterFields } from 'components/interfaces/DataTableDemo/default/constants'
-import { data } from 'components/interfaces/DataTableDemo/default/data'
-import { DataTable } from 'components/interfaces/DataTableDemo/default/data-table'
-import { Skeleton } from 'components/interfaces/DataTableDemo/default/skeleton'
-import { useSearchParams } from 'components/interfaces/DataTableDemo/default/search-params'
-
-export const LogPage: NextPageWithLayout = () => {
- const [search] = useSearchParams()
-
- return (
- }>
- ({
- id: key,
- value,
- }))
- .filter(({ value }) => value ?? undefined)}
- />
-
- )
-}
-
-LogPage.getLayout = (page) => (
-
- {page}
-
-)
-
-export default LogPage
diff --git a/apps/studio/pages/project/[ref]/logs-new.tsx b/apps/studio/pages/project/[ref]/logs-new.tsx
deleted file mode 100644
index 520889c8448..00000000000
--- a/apps/studio/pages/project/[ref]/logs-new.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
-// import { SearchParams } from 'nuqs' // No longer needed directly
-import * as React from 'react'
-import { Client } from 'components/interfaces/logs-new/client'
-import { dataOptions } from 'components/interfaces/logs-new/query-options-new'
-import { searchParamsParser } from 'components/interfaces/logs-new/search-params' // Import the parser
-// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
-import { useQueryStates } from 'nuqs' // Import useQueryStates
-
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
-
-import type { NextPageWithLayout } from 'types'
-import { useParams } from 'common'
-
-export const LogPage: NextPageWithLayout = () => {
- const { ref: projectRef } = useParams()
- // const searchParams = useSearchParams() // Replaced
- // const params = Object.fromEntries(searchParams.entries()) // Replaced
- // const search = searchParamsCache.parse(params) // Replaced
- const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
- const queryClient = getQueryClient()
-
- // Remove prefetching to avoid duplicate requests
- // React.useEffect(() => {
- // // Pass the parsed search object directly to dataOptions
- // queryClient.prefetchInfiniteQuery(dataOptions(search, projectRef ?? ''))
- // }, [search, queryClient, projectRef]) // Add queryClient and projectRef to dependencies
-
- return
-}
-
-LogPage.getLayout = (page) => (
-
- {page}
-
-)
-
-export default LogPage
diff --git a/apps/studio/pages/project/[ref]/logss.tsx b/apps/studio/pages/project/[ref]/logss.tsx
deleted file mode 100644
index 5b85c7fd29e..00000000000
--- a/apps/studio/pages/project/[ref]/logss.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
-// import { SearchParams } from 'nuqs' // No longer needed directly
-import * as React from 'react'
-import { Client } from 'components/interfaces/infinite-logs-example/client'
-import { dataOptions } from 'components/interfaces/DataTableDemo/light/query-options'
-import { searchParamsParser } from 'components/interfaces/DataTableDemo/light/search-params' // Import the parser
-// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
-import { useQueryStates } from 'nuqs' // Import useQueryStates
-
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
-
-import type { NextPageWithLayout } from 'types'
-
-export const LogPage: NextPageWithLayout = () => {
- // const searchParams = useSearchParams() // Replaced
- // const params = Object.fromEntries(searchParams.entries()) // Replaced
- // const search = searchParamsCache.parse(params) // Replaced
- const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
- const queryClient = getQueryClient()
-
- React.useEffect(() => {
- // Pass the parsed search object directly to dataOptions
- queryClient.prefetchInfiniteQuery(dataOptions(search))
- }, [search, queryClient]) // Add queryClient to dependencies
-
- return
-}
-
-LogPage.getLayout = (page) => (
-
- {page}
-
-)
-
-export default LogPage
diff --git a/apps/studio/pages/project/[ref]/unified-logs.tsx b/apps/studio/pages/project/[ref]/unified-logs.tsx
index 490f831a836..5e6bfb5f00c 100644
--- a/apps/studio/pages/project/[ref]/unified-logs.tsx
+++ b/apps/studio/pages/project/[ref]/unified-logs.tsx
@@ -1,15 +1,9 @@
-import { useRouter } from 'next/router'
+import { UnifiedLogsTable } from 'components/interfaces/UnifiedLogs/client'
+import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import type { NextPageWithLayout } from 'types'
-import DefaultLayout from 'components/layouts/DefaultLayout'
-import UnifiedLogsPreviewer from 'components/interfaces/UnifiedLogs/UnifiedLogsPreviewer'
-export const LogPage: NextPageWithLayout = () => {
- const router = useRouter()
- const { ref } = router.query
-
- return
-}
+export const LogPage: NextPageWithLayout = () =>
LogPage.getLayout = (page) => (