diff --git a/apps/docs/content/guides/telemetry/metrics.mdx b/apps/docs/content/guides/telemetry/metrics.mdx index 0309a85ab882e..eac18f6d9ad09 100644 --- a/apps/docs/content/guides/telemetry/metrics.mdx +++ b/apps/docs/content/guides/telemetry/metrics.mdx @@ -22,10 +22,18 @@ The endpoint discussed in this article is not available on self-hosted. ## Accessing the metrics endpoint -Your project's metrics endpoint is accessible at `https://.supabase.co/customer/v1/privileged/metrics`. Access to the endpoint is secured via HTTP Basic Auth; the username is `service_role`, while the password is the service role JWT available through the Supabase dashboard. +Your project's metrics endpoint is accessible at `https://.supabase.co/customer/v1/privileged/metrics`. + +Access to the endpoint is secured via HTTP Basic Auth: + +- username: `service_role` +- password: the service role JWT from the [Supabase dashboard](https://supabase.com/dashboard/project/_/settings/api-keys) + + ```shell -> curl https://.supabase.co/customer/v1/privileged/metrics --user 'service_role:' +curl /customer/v1/privileged/metrics \ + --user 'service_role:' ``` ## Supabase Grafana diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts index 06ee565334848..ef3597a06fbef 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.types.ts @@ -20,7 +20,6 @@ export interface LogsWarning { linkText?: string } export interface LogsEndpointParams { - project: string // project ref iso_timestamp_start?: string iso_timestamp_end?: string sql?: string diff --git a/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx b/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx index a5d22f4f968dc..1fda11e3d41cc 100644 --- a/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx +++ b/apps/studio/components/interfaces/SignIn/LastSignInWrapper.tsx @@ -1,7 +1,7 @@ import { LastSignInType, useLastSignIn } from 'hooks/misc/useLastSignIn' import { ReactNode } from 'react' -import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Badge, cn } from 'ui' export function LastSignInWrapper({ children, @@ -15,14 +15,12 @@ export function LastSignInWrapper({ return (
{lastSignIn === type && ( - - -
-
-
- - Last used - + + Last used + )}
> = { - totalRowCount: number - filterRowCount: number - chartData: BaseChartSchema[] - facets: Record - metadata?: TMeta -} - -type InfiniteQueryResponse = { - data: TData - meta: InfiniteQueryMeta - prevCursor: number | null - nextCursor: number | null -} - -export function createApiQueryString(params: Record): string { - const queryParams = new URLSearchParams() - - for (const [key, value] of Object.entries(params)) { - if (value === null || value === undefined) continue - - if (key === 'date' && Array.isArray(value) && value.length === 2) { - queryParams.set('dateStart', value[0].getTime().toString()) - queryParams.set('dateEnd', value[1].getTime().toString()) - } else if ( - [ - 'latency', - 'timing.dns', - 'timing.connection', - 'timing.tls', - 'timing.ttfb', - 'timing.transfer', - 'status', - ].includes(key) && - Array.isArray(value) && - value.length > 0 - ) { - if (value.length >= 2) { - queryParams.set(`${key}Start`, value[0].toString()) - queryParams.set(`${key}End`, value[value.length - 1].toString()) - } - } else if (Array.isArray(value)) { - if (value.length > 0) { - queryParams.set(key, value.join(ARRAY_DELIMITER)) - } - } else if (key === 'sort' && typeof value === 'object' && value !== null) { - queryParams.set(key, `${value.id}${SORT_DELIMITER}${value.desc ? 'desc' : 'asc'}`) - } else if (value instanceof Date) { - queryParams.set(key, value.getTime().toString()) - } else { - queryParams.set(key, String(value)) - } - } - - const queryString = queryParams.toString() - return queryString ? `?${queryString}` : '' -} - -// Add a new function to fetch chart data for the entire time period -export const useChartData = (search: SearchParamsType, projectRef: string) => { - // Create a stable query key object by removing nulls/undefined, uuid, and live - const queryKeyParams = Object.entries(search).reduce( - (acc, [key, value]) => { - if (!['uuid', 'live'].includes(key) && value !== null && value !== undefined) { - acc[key] = value - } - return acc - }, - {} as Record - ) - - return useQuery({ - queryKey: [ - 'unified-logs-chart', - projectRef, - // Use JSON.stringify for a stable key representation - JSON.stringify(queryKeyParams), - ], - queryFn: async () => { - try { - // Use a default date range (last hour) if no date range is selected - let dateStart: string - let dateEnd: string - let startTime: Date - let endTime: Date - - if (search.date && search.date.length === 2) { - startTime = new Date(search.date[0]) - endTime = new Date(search.date[1]) - dateStart = startTime.toISOString() - dateEnd = endTime.toISOString() - } else { - // Default to last hour - endTime = new Date() - startTime = new Date(endTime.getTime() - 60 * 60 * 1000) - dateStart = startTime.toISOString() - dateEnd = endTime.toISOString() - } - - // Get SQL query from utility function (with dynamic bucketing) - const sql = getLogsChartQuery(search) - - // Use the get function from data/fetchers for chart data - const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, { - params: { - path: { ref: projectRef }, - query: { - iso_timestamp_start: dateStart, - iso_timestamp_end: dateEnd, - project: projectRef, - sql, - }, - }, - }) - - if (error) { - if (DEBUG_MODE) console.error('API returned error for chart data:', error) - throw error - } - - // Process API results directly without additional bucketing - const chartData: Array<{ - timestamp: number - success: number - warning: number - error: number - }> = [] - - // Create a map to store data points by their timestamp - const dataByTimestamp = new Map< - number, - { - timestamp: number - success: number - warning: number - error: number - } - >() - - // Track the total count from the query results - // this uses the total_per_bucket field that was added to the chart query - let totalCount = 0 - - // Only process API results if we have them - if (data?.result) { - data.result.forEach((row: any) => { - // The API returns timestamps in microseconds (needs to be converted to milliseconds for JS Date) - const microseconds = Number(row.time_bucket) - const milliseconds = Math.floor(microseconds / 1000) - - // Add to total count - this comes directly from the query - totalCount += Number(row.total_per_bucket || 0) - - // Create chart data point - const dataPoint = { - timestamp: milliseconds, // Convert to milliseconds for the chart - success: Number(row.success) || 0, - warning: Number(row.warning) || 0, - error: Number(row.error) || 0, - } - - // Filter levels if needed - const levelFilter = search.level - if (levelFilter && levelFilter.length > 0) { - // Reset levels not in the filter - if (!levelFilter.includes('success')) dataPoint.success = 0 - if (!levelFilter.includes('warning')) dataPoint.warning = 0 - if (!levelFilter.includes('error')) dataPoint.error = 0 - } - - dataByTimestamp.set(milliseconds, dataPoint) - }) - } - - // Determine bucket size based on the truncation level in the SQL query - // We need to fill in missing data points - const startTimeMs = startTime.getTime() - const endTimeMs = endTime.getTime() - - // Calculate appropriate bucket size from the time range - const timeRangeHours = (endTimeMs - startTimeMs) / (1000 * 60 * 60) - - let bucketSizeMs: number - if (timeRangeHours > 72) { - // Day-level bucketing (for ranges > 3 days) - bucketSizeMs = 24 * 60 * 60 * 1000 - } else if (timeRangeHours > 12) { - // Hour-level bucketing (for ranges > 12 hours) - bucketSizeMs = 60 * 60 * 1000 - } else { - // Minute-level bucketing (for shorter ranges) - bucketSizeMs = 60 * 1000 - } - - // Fill in any missing buckets - for (let t = startTimeMs; t <= endTimeMs; t += bucketSizeMs) { - // Round to the nearest bucket boundary - const bucketTime = Math.floor(t / bucketSizeMs) * bucketSizeMs - - if (!dataByTimestamp.has(bucketTime)) { - // Create empty data point for this bucket - dataByTimestamp.set(bucketTime, { - timestamp: bucketTime, - success: 0, - warning: 0, - error: 0, - }) - } - } - - // Convert map to array - for (const dataPoint of dataByTimestamp.values()) { - chartData.push(dataPoint) - } - - // Sort by timestamp - chartData.sort((a, b) => a.timestamp - b.timestamp) - - // Add debugging info for totalCount - if (DEBUG_MODE) console.log(`Total count from chart query: ${totalCount}`) - - return { - chartData, - totalCount, - } - } catch (error) { - if (DEBUG_MODE) console.error('Error fetching chart data:', error) - throw error - } - }, - // Keep chart data fresh for 5 minutes - staleTime: 1000 * 60 * 5, - }) -} - -// The existing dataOptions function remains with buildChartData fallback -export const dataOptions = (search: SearchParamsType, projectRef: string) => { - // Create a stable query key object by removing nulls/undefined, uuid, and live - const queryKeyParams = Object.entries(search).reduce( - (acc, [key, value]) => { - if (!['uuid', 'live'].includes(key) && value !== null && value !== undefined) { - acc[key] = value - } - return acc - }, - {} as Record - ) - - // 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() - // } - // } - - // Simply return the options object - return { - queryKey: [ - 'unified-logs', - projectRef, - // Use JSON.stringify for a stable key representation - JSON.stringify(queryKeyParams), - ], - queryFn: async ({ pageParam }: { pageParam?: PageParam }) => { - try { - const cursorValue = pageParam?.cursor // Already in microseconds - const direction = pageParam?.direction - const isPagination = pageParam !== undefined - - // Extract date range from search or use default (last hour) - let isoTimestampStart: string - let isoTimestampEnd: string - - if (search.date && search.date.length === 2) { - isoTimestampStart = new Date(search.date[0]).toISOString() - isoTimestampEnd = new Date(search.date[1]).toISOString() - } else { - // Default to last hour - const now = new Date() - isoTimestampEnd = now.toISOString() - const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000) - isoTimestampStart = oneHourAgo.toISOString() - } - - // Get the unified SQL query for logs data from utility function - let logsSql = getUnifiedLogsQuery(search) - - // Add ordering and limit - logsSql += `\nORDER BY timestamp DESC, id DESC` - logsSql += `\nLIMIT 50` - - // Get SQL query for counts from utility function - const countsSql = getLogsCountQuery(search) - - // First, fetch the counts data - const { data: countsData, error: countsError } = await get( - `/platform/projects/{ref}/analytics/endpoints/logs.all`, - { - params: { - path: { ref: projectRef }, - query: { - iso_timestamp_start: isoTimestampStart, - iso_timestamp_end: isoTimestampEnd, - project: projectRef, - sql: countsSql, - }, - }, - } - ) - - if (countsError) { - if (DEBUG_MODE) console.error('API returned error for counts data:', countsError) - throw countsError - } - - // Process count results into facets structure - const facets: Record = {} - const countsByDimension: Record> = {} - let totalCount = 0 - - // Group by dimension - if (countsData?.result) { - countsData.result.forEach((row: any) => { - const dimension = row.dimension - const value = row.value - const count = Number(row.count || 0) - - // Set total count if this is the total dimension - if (dimension === 'total' && value === 'all') { - totalCount = count - } - - // Initialize dimension map if not exists - if (!countsByDimension[dimension]) { - countsByDimension[dimension] = new Map() - } - - // Add count to the dimension map - countsByDimension[dimension].set(value, count) - }) - } - - // Convert dimension maps to facets structure - Object.entries(countsByDimension).forEach(([dimension, countsMap]) => { - // Skip the 'total' dimension as it's not a facet - if (dimension === 'total') return - - const dimensionTotal = Array.from(countsMap.values()).reduce( - (sum, count) => sum + count, - 0 - ) - - facets[dimension] = { - min: undefined, - max: undefined, - total: dimensionTotal, - rows: Array.from(countsMap.entries()).map(([value, count]) => ({ - value, - total: count, - })), - } - }) - - // Now, fetch the logs data with pagination - // ONLY convert to ISO when we're about to send to the API - let timestampStart: string - let timestampEnd: string - - if (isPagination && direction === 'prev') { - // Live mode: fetch logs newer than the cursor - timestampStart = cursorValue - ? new Date(Number(cursorValue) / 1000).toISOString() // Convert microseconds to ISO for API - : isoTimestampStart - timestampEnd = new Date().toISOString() // Current time as ISO for API - } else if (isPagination && direction === 'next') { - // Regular pagination: fetch logs older than the cursor - timestampStart = isoTimestampStart - timestampEnd = cursorValue - ? new Date(Number(cursorValue) / 1000).toISOString() // Convert microseconds to ISO for API - : isoTimestampEnd - } else { - // Initial load: use the original date range - timestampStart = isoTimestampStart - timestampEnd = isoTimestampEnd - } - - if (DEBUG_MODE) { - console.log( - `🔍 Query function called: isPagination=${isPagination}, cursorValue=${cursorValue}, direction=${direction}, iso_timestamp_start=${timestampStart}, iso_timestamp_end=${timestampEnd}` - ) - console.log('🔍 Raw pageParam received:', pageParam) - } - - const { data: logsData, error: logsError } = await get( - `/platform/projects/{ref}/analytics/endpoints/logs.all`, - { - params: { - path: { ref: projectRef }, - query: { - iso_timestamp_start: timestampStart, - iso_timestamp_end: timestampEnd, - project: projectRef, - sql: logsSql, - }, - }, - } - ) - - if (logsError) { - if (DEBUG_MODE) console.error('API returned error for logs data:', logsError) - throw logsError - } - - // Process the logs results - const resultData = logsData?.result || [] - - if (DEBUG_MODE) console.log(`Received ${resultData.length} records from API`) - - // Define specific level types - type LogLevel = 'success' | 'warning' | 'error' - - // Transform results to expected schema - const result = resultData.map((row: any) => { - // Create a unique ID using the timestamp - const uniqueId = `${row.id || 'id'}-${row.timestamp}-${new Date().getTime()}` - - // Create a date object for display purposes - // The timestamp is in microseconds, need to convert to milliseconds for JS Date - const date = new Date(Number(row.timestamp) / 1000) - - // Use the level directly from SQL rather than determining it in TypeScript - const level = row.level as LogLevel - - return { - id: uniqueId, - uuid: uniqueId, - date, // Date object for display purposes - timestamp: row.timestamp, // Original timestamp from the database - level, - status: row.status || 200, - method: row.method, - host: row.host, - pathname: (row.url || '').replace(/^https?:\/\/[^\/]+/, '') || row.path || '', - event_message: row.event_message || row.body || '', - headers: - typeof row.headers === 'string' ? JSON.parse(row.headers || '{}') : row.headers || {}, - regions: row.region ? [row.region] : [], - log_type: row.log_type || '', - latency: row.latency || 0, - log_count: row.log_count || null, - logs: row.logs || [], - auth_user: row.auth_user || null, - } - }) - - // Just use the row timestamps directly for cursors - const lastRow = result.length > 0 ? result[result.length - 1] : null - const firstRow = result.length > 0 ? result[0] : null - const nextCursor = lastRow ? lastRow.timestamp : null - - // FIXED: Always provide prevCursor like DataTableDemo does - // This ensures live mode never breaks the infinite query chain - // DataTableDemo uses milliseconds, but our timestamps are in microseconds - const prevCursor = result.length > 0 ? firstRow!.timestamp : new Date().getTime() * 1000 - - // Determine if there might be more data - const pageLimit = 50 - - // HACK: Backend uses "timestamp > cursor" which can exclude records with identical timestamps - // THIS CAN SOMETIMES CAUSE 49 RECORDS INSTEAD OF 50 TO BE RETURNED - // TODO: Revisit this - ideally the backend should use composite cursors (timestamp+id) for proper pagination - // For now, we consider 49 records as a "full page" to ensure pagination works correctly - const hasMore = result.length >= pageLimit - 1 // Consider 49 or 50 records as a full page - - if (DEBUG_MODE) { - console.log( - `Pagination info: result.length=${result.length}, hasMore=${hasMore}, nextCursor=${nextCursor}, prevCursor=${prevCursor}` - ) - } - - // Create response with pagination info - const response = { - data: result, - meta: { - // Use the total count from our counts query - totalRowCount: totalCount, - filterRowCount: result.length, - chartData: buildChartData(result), - facets, // Use the facets from the counts query - metadata: { - currentPercentiles: {}, - logTypeCounts: calculateLogTypeCounts(result), - }, - }, - - nextCursor: hasMore ? nextCursor : null, - prevCursor, - } - - return response - } catch (error) { - if (DEBUG_MODE) console.error('Error fetching unified logs:', error) - throw error - } - }, - // Initial load with proper cursor for live mode pagination - // Use microseconds format to match database timestamps - initialPageParam: { cursor: new Date().getTime() * 1000, direction: 'next' } as PageParam, - getPreviousPageParam: ( - firstPage: InfiniteQueryResponse - ) => { - if (DEBUG_MODE) { - console.log('🔍 getPreviousPageParam called with:', { - hasFirstPage: !!firstPage, - dataLength: firstPage?.data?.length || 0, - prevCursor: firstPage?.prevCursor, - nextCursor: firstPage?.nextCursor, - firstPageKeys: firstPage ? Object.keys(firstPage) : 'no firstPage', - }) - } - - // Use the same logic as the working DataTableDemo - if (!firstPage.prevCursor) { - if (DEBUG_MODE) console.log('🔍 getPreviousPageParam returning: null (no prevCursor)') - return null - } - - const result = { cursor: firstPage.prevCursor, direction: 'prev' } as PageParam - if (DEBUG_MODE) console.log('🔍 getPreviousPageParam returning:', result) - return result - }, - getNextPageParam: ( - lastPage: InfiniteQueryResponse, - allPages: InfiniteQueryResponse[] - ) => { - // Only return a cursor if we actually have more data to fetch - if (!lastPage.nextCursor || lastPage.data.length === 0) return null - // Only trigger fetch when specifically requested, not during column resizing - return { cursor: lastPage.nextCursor, direction: 'next' } as PageParam - }, - // Configure React Query to be more stable - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - refetchInterval: 0, - staleTime: 1000 * 60 * 5, // 5 minutes - gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime) - } -} - -// Helper functions for chart data and log type counts -function buildChartData(logs: any[]) { - // Group logs by minute and count by level - const chartData = [] - const minuteGroups = new Map() - - // Sort by timestamp ascending - const sortedLogs = [...logs].sort((a, b) => a.rawTimestamp - b.rawTimestamp) - - for (const log of sortedLogs) { - const minute = dayjs(log.date).startOf('minute').valueOf() - - if (!minuteGroups.has(minute)) { - minuteGroups.set(minute, { - // Use number timestamp instead of Date object - timestamp: minute, - success: 0, - error: 0, - warning: 0, - }) - } - - const group = minuteGroups.get(minute) - // Ensure we're only using valid log levels that match the schema - const level = ['success', 'error', 'warning'].includes(log.level) ? log.level : 'success' - group[level] = (group[level] || 0) + 1 - } - - // Convert map to array - for (const data of minuteGroups.values()) { - chartData.push(data) - } - - return chartData.sort((a, b) => a.timestamp - b.timestamp) -} - -function calculateLogTypeCounts(logs: any[]) { - const counts: Record = {} - - logs.forEach((log) => { - const logType = log.log_type - counts[logType] = (counts[logType] || 0) + 1 - }) - - return counts -} diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts index d2823d0d126e4..8156978dfd528 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs' -import { SearchParamsType } from './UnifiedLogs.types' +import { QuerySearchParamsType, SearchParamsType } from './UnifiedLogs.types' // Pagination and control parameters const PAGINATION_PARAMS = ['sort', 'start', 'size', 'uuid', 'cursor', 'direction', 'live'] as const @@ -17,7 +17,7 @@ const BASE_CONDITIONS_EXCLUDED_PARAMS = [...PAGINATION_PARAMS, 'date', 'level'] * @param search SearchParamsType object containing query parameters * @returns Object with whereConditions array and formatted WHERE clause */ -const buildQueryConditions = (search: SearchParamsType) => { +const buildQueryConditions = (search: QuerySearchParamsType) => { const whereConditions: string[] = [] // Process all search parameters for filtering @@ -465,7 +465,7 @@ WITH unified_logs AS ( /** * Unified logs SQL query */ -export const getUnifiedLogsQuery = (search: SearchParamsType): string => { +export const getUnifiedLogsQuery = (search: QuerySearchParamsType): string => { // Use the buildQueryConditions helper const { finalWhere } = buildQueryConditions(search) @@ -497,7 +497,7 @@ ${finalWhere} * Get a count query for the total logs within the timeframe * Also returns facets for all filter dimensions */ -export const getLogsCountQuery = (search: SearchParamsType): string => { +export const getLogsCountQuery = (search: QuerySearchParamsType): string => { // Use the buildQueryConditions helper const { finalWhere } = buildQueryConditions(search) @@ -542,9 +542,9 @@ GROUP BY method * Enhanced logs chart query with dynamic bucketing based on time range * Incorporates dynamic bucketing from the older implementation */ -export const getLogsChartQuery = (search: SearchParamsType | Record): string => { +export const getLogsChartQuery = (search: QuerySearchParamsType): string => { // Use the buildQueryConditions helper - const { finalWhere } = buildQueryConditions(search as SearchParamsType) + const { finalWhere } = buildQueryConditions(search) // Determine appropriate bucketing level based on time range const truncationLevel = calculateChartBucketing(search) diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx index 3344d5d77e801..ed6128be00c2a 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx @@ -1,4 +1,3 @@ -import { useInfiniteQuery } from '@tanstack/react-query' import { ColumnFiltersState, getCoreRowModel, @@ -30,6 +29,9 @@ import { LiveRow } from 'components/ui/DataTable/LiveRow' import { DataTableProvider } from 'components/ui/DataTable/providers/DataTableProvider' import { RefreshButton } from 'components/ui/DataTable/RefreshButton' import { TimelineChart } from 'components/ui/DataTable/TimelineChart' +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 { ChartConfig, @@ -46,10 +48,10 @@ import { import { COLUMNS } from './components/Columns' import { MemoizedDataTableSheetContent } from './components/DataTableSheetContent' import { FunctionLogsTab } from './components/FunctionLogsTab' -import { dataOptions, useChartData } from './QueryOptions' import { CHART_CONFIG, SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants' import { filterFields as defaultFilterFields, sheetFields } from './UnifiedLogs.fields' import { useLiveMode, useResetFocus } from './UnifiedLogs.hooks' +import { QuerySearchParamsType } from './UnifiedLogs.types' import { getFacetedUniqueValues, getLevelRowClassName, logEventBus } from './UnifiedLogs.utils' // Debug mode flag - set to true to enable detailed logs @@ -84,29 +86,47 @@ export const UnifiedLogs = () => { [] ) - // [Joshen] This needs to move to the data folder to follow our proper RQ structure - const { data, isFetching, isLoading, fetchNextPage, hasNextPage, fetchPreviousPage, refetch } = - // @ts-ignore - useInfiniteQuery(dataOptions(search, projectRef ?? '')) + // Create a stable query key object by removing nulls/undefined, uuid, and live + // Mainly to prevent the react queries from unnecessarily re-fetching + const searchParameters = Object.entries(search).reduce( + (acc, [key, value]) => { + if (!['uuid', 'live'].includes(key) && value !== null && value !== undefined) { + acc[key] = value + } + return acc + }, + {} as Record + ) as QuerySearchParamsType + + const { + data: unifiedLogsData, + isLoading, + isFetching, + hasNextPage, + refetch, + fetchNextPage, + fetchPreviousPage, + } = useUnifiedLogsInfiniteQuery({ projectRef, search: searchParameters }) + const { data: counts } = useUnifiedLogsCountQuery({ projectRef, search: searchParameters }) + const { data: unifiedLogsChart = [] } = useUnifiedLogsChartQuery({ + projectRef, + search: searchParameters, + }) const flatData = useMemo(() => { - return data?.pages?.flatMap((page) => page.data ?? []) ?? [] - }, [data?.pages]) + return unifiedLogsData?.pages?.flatMap((page) => page.data ?? []) ?? [] + }, [unifiedLogsData?.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] + const lastPage = unifiedLogsData?.pages?.[unifiedLogsData?.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 totalDBRowCount = counts?.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 facets = counts?.facets const totalFetched = flatData?.length // Create a filtered version of the chart config based on selected levels @@ -152,18 +172,13 @@ export const UnifiedLogs = () => { 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]) + }, [rowSelection, flatData]) const selectedRowKey = Object.keys(rowSelection)?.[0] @@ -247,7 +262,6 @@ export const UnifiedLogs = () => { // 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) { @@ -283,7 +297,6 @@ export const UnifiedLogs = () => { enableColumnOrdering={true} isLoading={isFetching || isLoading} getFacetedUniqueValues={getFacetedUniqueValues(facets)} - // getFacetedMinMaxValues={getFacetedMinMaxValues(facets)} > @@ -303,7 +316,7 @@ export const UnifiedLogs = () => { ]} /> { totalRows: totalDBRowCount ?? 0, filterRows: filterDBRowCount ?? 0, totalRowsFetched: totalFetched ?? 0, - currentPercentiles: metadata?.currentPercentiles ?? ({} as any), - ...metadata, + currentPercentiles: {} as any, }} /> diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.types.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.types.ts index d041f8c22efa2..22dc02b95525e 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.types.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.types.ts @@ -23,14 +23,10 @@ export type LogsMeta = { currentPercentiles: Record } -export type UnifiedLogsMeta = { - logTypeCounts: Record - currentPercentiles: Record -} - export type PageParam = { cursor: number; direction: 'next' | 'prev' } export type SearchParamsType = inferParserType +export type QuerySearchParamsType = Omit export type SearchParams = { [key: string]: string | string[] | undefined diff --git a/apps/studio/components/interfaces/UnifiedLogs/components/DataTableSheetContent.tsx b/apps/studio/components/interfaces/UnifiedLogs/components/DataTableSheetContent.tsx index cdd1800a1d5e6..ff08c51e68172 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/components/DataTableSheetContent.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/components/DataTableSheetContent.tsx @@ -1,5 +1,5 @@ import { Table } from '@tanstack/react-table' -import { memo } from 'react' +import { HTMLAttributes, memo } from 'react' import { DataTableFilterField } from 'components/ui/DataTable/DataTable.types' import { DataTableSheetRowAction } from 'components/ui/DataTable/DataTableSheetRowAction' @@ -30,7 +30,7 @@ const SheetDetailsContentSkeleton = ({ ) } -interface DataTableSheetContentProps extends React.HTMLAttributes { +interface DataTableSheetContentProps extends HTMLAttributes { data?: TData table: Table fields: SheetField[] diff --git a/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx b/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx index 0c1009279c3cf..f351dfe298159 100644 --- a/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx +++ b/apps/studio/components/ui/DataTable/providers/DataTableProvider.tsx @@ -7,7 +7,7 @@ import type { Table, VisibilityState, } from '@tanstack/react-table' -import { createContext, useContext, useMemo } from 'react' +import { createContext, ReactNode, useContext, useMemo } from 'react' import { DataTableFilterField } from '../DataTable.types' import { ControlsProvider } from './ControlsProvider' @@ -45,7 +45,7 @@ export function DataTableProvider({ ...props }: Partial & DataTableBaseContextType & { - children: React.ReactNode + children: ReactNode }) { const value = useMemo( () => ({ diff --git a/apps/studio/data/logs/keys.ts b/apps/studio/data/logs/keys.ts new file mode 100644 index 0000000000000..2a02591a244af --- /dev/null +++ b/apps/studio/data/logs/keys.ts @@ -0,0 +1,37 @@ +import { QuerySearchParamsType } from 'components/interfaces/UnifiedLogs/UnifiedLogs.types' + +export const logsKeys = { + unifiedLogsInfinite: ( + projectRef: string | undefined, + searchParams: QuerySearchParamsType | undefined + ) => + [ + 'projects', + projectRef, + 'unified-logs', + 'logs-data', + ...(searchParams ? [searchParams].filter(Boolean) : []), + ] as const, + unifiedLogsCount: ( + projectRef: string | undefined, + searchParams: QuerySearchParamsType | undefined + ) => + [ + 'projects', + projectRef, + 'unified-logs', + 'count-data', + ...(searchParams ? [searchParams].filter(Boolean) : []), + ] as const, + unifiedLogsChart: ( + projectRef: string | undefined, + searchParams: QuerySearchParamsType | undefined + ) => + [ + 'projects', + projectRef, + 'unified-logs', + 'chart-data', + ...(searchParams ? [searchParams].filter(Boolean) : []), + ] as const, +} diff --git a/apps/studio/data/logs/unified-logs-chart-query.ts b/apps/studio/data/logs/unified-logs-chart-query.ts new file mode 100644 index 0000000000000..de215cf36eadf --- /dev/null +++ b/apps/studio/data/logs/unified-logs-chart-query.ts @@ -0,0 +1,156 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { getLogsChartQuery } from 'components/interfaces/UnifiedLogs/UnifiedLogs.queries' +import { handleError, post } from 'data/fetchers' +import { ExecuteSqlError } from 'data/sql/execute-sql-query' +import { logsKeys } from './keys' +import { UNIFIED_LOGS_STALE_TIME, UnifiedLogsVariables } from './unified-logs-infinite-query' + +export async function getUnifiedLogsChart( + { projectRef, search }: UnifiedLogsVariables, + signal?: AbortSignal +) { + if (typeof projectRef === 'undefined') { + throw new Error('projectRef is required for getUnifiedLogsChart') + } + + // Use a default date range (last hour) if no date range is selected + let dateStart: string + let dateEnd: string + let startTime: Date + let endTime: Date + + if (search.date && search.date.length === 2) { + startTime = new Date(search.date[0]) + endTime = new Date(search.date[1]) + dateStart = startTime.toISOString() + dateEnd = endTime.toISOString() + } else { + // Default to last hour + endTime = new Date() + startTime = new Date(endTime.getTime() - 60 * 60 * 1000) + dateStart = startTime.toISOString() + dateEnd = endTime.toISOString() + } + + // Get SQL query from utility function (with dynamic bucketing) + const sql = getLogsChartQuery(search) + + const { data, error } = await post(`/platform/projects/{ref}/analytics/endpoints/logs.all`, { + params: { path: { ref: projectRef } }, + body: { sql, iso_timestamp_start: dateStart, iso_timestamp_end: dateEnd }, + signal, + }) + + if (error) handleError(error) + + const chartData: Array<{ + timestamp: number + success: number + warning: number + error: number + }> = [] + + const dataByTimestamp = new Map< + number, + { + timestamp: number + success: number + warning: number + error: number + } + >() + + if (data?.result) { + data.result.forEach((row: any) => { + // The API returns timestamps in microseconds (needs to be converted to milliseconds for JS Date) + const microseconds = Number(row.time_bucket) + const milliseconds = Math.floor(microseconds / 1000) + + // Create chart data point + const dataPoint = { + timestamp: milliseconds, // Convert to milliseconds for the chart + success: Number(row.success) || 0, + warning: Number(row.warning) || 0, + error: Number(row.error) || 0, + } + + // Filter levels if needed + const levelFilter = search.level + if (levelFilter && levelFilter.length > 0) { + // Reset levels not in the filter + if (!levelFilter.includes('success')) dataPoint.success = 0 + if (!levelFilter.includes('warning')) dataPoint.warning = 0 + if (!levelFilter.includes('error')) dataPoint.error = 0 + } + + dataByTimestamp.set(milliseconds, dataPoint) + }) + } + + // Determine bucket size based on the truncation level in the SQL query + // We need to fill in missing data points + const startTimeMs = startTime.getTime() + const endTimeMs = endTime.getTime() + + // Calculate appropriate bucket size from the time range + const timeRangeHours = (endTimeMs - startTimeMs) / (1000 * 60 * 60) + + let bucketSizeMs: number + if (timeRangeHours > 72) { + // Day-level bucketing (for ranges > 3 days) + bucketSizeMs = 24 * 60 * 60 * 1000 + } else if (timeRangeHours > 12) { + // Hour-level bucketing (for ranges > 12 hours) + bucketSizeMs = 60 * 60 * 1000 + } else { + // Minute-level bucketing (for shorter ranges) + bucketSizeMs = 60 * 1000 + } + + // Fill in any missing buckets + for (let t = startTimeMs; t <= endTimeMs; t += bucketSizeMs) { + // Round to the nearest bucket boundary + const bucketTime = Math.floor(t / bucketSizeMs) * bucketSizeMs + + if (!dataByTimestamp.has(bucketTime)) { + // Create empty data point for this bucket + dataByTimestamp.set(bucketTime, { + timestamp: bucketTime, + success: 0, + warning: 0, + error: 0, + }) + } + } + + // Convert map to array + for (const dataPoint of dataByTimestamp.values()) { + chartData.push(dataPoint) + } + + // Sort by timestamp + chartData.sort((a, b) => a.timestamp - b.timestamp) + + return chartData +} + +export type UnifiedLogsChartData = Awaited> +export type UnifiedLogsChartError = ExecuteSqlError + +export const useUnifiedLogsChartQuery = ( + { projectRef, search }: UnifiedLogsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + logsKeys.unifiedLogsChart(projectRef, search), + ({ signal }) => getUnifiedLogsChart({ projectRef, search }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined', + staleTime: UNIFIED_LOGS_STALE_TIME, + ...options, + } + ) diff --git a/apps/studio/data/logs/unified-logs-count-query.ts b/apps/studio/data/logs/unified-logs-count-query.ts new file mode 100644 index 0000000000000..cca71aa39fcc5 --- /dev/null +++ b/apps/studio/data/logs/unified-logs-count-query.ts @@ -0,0 +1,94 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { getLogsCountQuery } from 'components/interfaces/UnifiedLogs/UnifiedLogs.queries' +import { FacetMetadataSchema } from 'components/interfaces/UnifiedLogs/UnifiedLogs.schema' +import { handleError, post } from 'data/fetchers' +import { ExecuteSqlError } from 'data/sql/execute-sql-query' +import { logsKeys } from './keys' +import { + getUnifiedLogsISOStartEnd, + UNIFIED_LOGS_STALE_TIME, + UnifiedLogsVariables, +} from './unified-logs-infinite-query' + +export async function getUnifiedLogsCount( + { projectRef, search }: UnifiedLogsVariables, + signal?: AbortSignal +) { + if (typeof projectRef === 'undefined') { + throw new Error('projectRef is required for getUnifiedLogsCount') + } + + const sql = getLogsCountQuery(search) + const { isoTimestampStart, isoTimestampEnd } = getUnifiedLogsISOStartEnd(search) + + const { data, error } = await post(`/platform/projects/{ref}/analytics/endpoints/logs.all`, { + params: { path: { ref: projectRef } }, + body: { iso_timestamp_start: isoTimestampStart, iso_timestamp_end: isoTimestampEnd, sql }, + signal, + }) + + if (error) handleError(error) + + // Process count results into facets structure + const facets: Record = {} + const countsByDimension: Record> = {} + let totalRowCount = 0 + + // Group by dimension + if (data?.result) { + data.result.forEach((row: any) => { + const dimension = row.dimension + const value = row.value + const count = Number(row.count || 0) + + // Set total count if this is the total dimension + if (dimension === 'total' && value === 'all') { + totalRowCount = count + } + + // Initialize dimension map if not exists + if (!countsByDimension[dimension]) { + countsByDimension[dimension] = new Map() + } + + // Add count to the dimension map + countsByDimension[dimension].set(value, count) + }) + } + + // Convert dimension maps to facets structure + Object.entries(countsByDimension).forEach(([dimension, countsMap]) => { + // Skip the 'total' dimension as it's not a facet + if (dimension === 'total') return + + const dimensionTotal = Array.from(countsMap.values()).reduce((sum, count) => sum + count, 0) + + facets[dimension] = { + total: dimensionTotal, + rows: Array.from(countsMap.entries()).map(([value, count]) => ({ value, total: count })), + } + }) + + return { totalRowCount, facets } +} + +export type UnifiedLogsCountData = Awaited> +export type UnifiedLogsCountError = ExecuteSqlError + +export const useUnifiedLogsCountQuery = ( + { projectRef, search }: UnifiedLogsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + logsKeys.unifiedLogsCount(projectRef, search), + ({ signal }) => getUnifiedLogsCount({ projectRef, search }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined', + staleTime: UNIFIED_LOGS_STALE_TIME, + ...options, + } + ) diff --git a/apps/studio/data/logs/unified-logs-infinite-query.ts b/apps/studio/data/logs/unified-logs-infinite-query.ts new file mode 100644 index 0000000000000..c7afe83a21d53 --- /dev/null +++ b/apps/studio/data/logs/unified-logs-infinite-query.ts @@ -0,0 +1,174 @@ +import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query' + +import { getUnifiedLogsQuery } from 'components/interfaces/UnifiedLogs/UnifiedLogs.queries' +import { + PageParam, + QuerySearchParamsType, +} from 'components/interfaces/UnifiedLogs/UnifiedLogs.types' +import { handleError, post } from 'data/fetchers' +import { ResponseError } from 'types' +import { logsKeys } from './keys' + +const LOGS_PAGE_LIMIT = 50 +type LogLevel = 'success' | 'warning' | 'error' +export const UNIFIED_LOGS_STALE_TIME = 1000 * 60 * 5 // 5 minutes + +export type UnifiedLogsData = any +export type UnifiedLogsError = ResponseError +export type UnifiedLogsVariables = { projectRef?: string; search: QuerySearchParamsType } + +export const getUnifiedLogsISOStartEnd = (search: QuerySearchParamsType) => { + // Extract date range from search or use default (last hour) + let isoTimestampStart: string + let isoTimestampEnd: string + + if (search.date && search.date.length === 2) { + isoTimestampStart = new Date(search.date[0]).toISOString() + isoTimestampEnd = new Date(search.date[1]).toISOString() + } else { + // Default to last hour + const now = new Date() + isoTimestampEnd = now.toISOString() + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000) + isoTimestampStart = oneHourAgo.toISOString() + } + + return { isoTimestampStart, isoTimestampEnd } +} + +/** + * Refactor notes + * - Shouldn't need to handle "direction", we store all data as it gets infinitely feteched + * - Shouldn't need to handle fetching previous too i think + */ + +async function getUnifiedLogs( + { projectRef, search, pageParam }: UnifiedLogsVariables & { pageParam: PageParam }, + signal?: AbortSignal +) { + if (typeof projectRef === 'undefined') + throw new Error('projectRef is required for getUnifiedLogs') + + const cursorValue = pageParam?.cursor // Already in microseconds + const direction = pageParam?.direction + const isPagination = pageParam !== undefined + const sql = `${getUnifiedLogsQuery(search)} ORDER BY timestamp DESC, id DESC LIMIT ${LOGS_PAGE_LIMIT}` + + const { isoTimestampStart, isoTimestampEnd } = getUnifiedLogsISOStartEnd(search) + + let timestampStart: string + let timestampEnd: string + + if (isPagination && direction === 'prev') { + // Live mode: fetch logs newer than the cursor + timestampStart = cursorValue + ? new Date(Number(cursorValue) / 1000).toISOString() // Convert microseconds to ISO for API + : isoTimestampStart + timestampEnd = new Date().toISOString() // Current time as ISO for API + } else if (isPagination && direction === 'next') { + // Regular pagination: fetch logs older than the cursor + timestampStart = isoTimestampStart + timestampEnd = cursorValue + ? new Date(Number(cursorValue) / 1000).toISOString() // Convert microseconds to ISO for API + : isoTimestampEnd + } else { + // Initial load: use the original date range + timestampStart = isoTimestampStart + timestampEnd = isoTimestampEnd + } + + const { data, error } = await post(`/platform/projects/{ref}/analytics/endpoints/logs.all`, { + params: { path: { ref: projectRef } }, + body: { iso_timestamp_start: timestampStart, iso_timestamp_end: timestampEnd, sql }, + signal, + }) + + if (error) handleError(error) + + const resultData = data?.result || [] + + // Transform results to expected schema + const result = resultData.map((row: any) => { + // Create a unique ID using the timestamp + const uniqueId = `${row.id || 'id'}-${row.timestamp}-${new Date().getTime()}` + + // Create a date object for display purposes + // The timestamp is in microseconds, need to convert to milliseconds for JS Date + const date = new Date(Number(row.timestamp) / 1000) + + // Use the level directly from SQL rather than determining it in TypeScript + const level = row.level as LogLevel + + return { + id: uniqueId, + uuid: uniqueId, + date, // Date object for display purposes + timestamp: row.timestamp, // Original timestamp from the database + level, + status: row.status || 200, + method: row.method, + host: row.host, + pathname: (row.url || '').replace(/^https?:\/\/[^\/]+/, '') || row.path || '', + event_message: row.event_message || row.body || '', + headers: + typeof row.headers === 'string' ? JSON.parse(row.headers || '{}') : row.headers || {}, + regions: row.region ? [row.region] : [], + log_type: row.log_type || '', + latency: row.latency || 0, + log_count: row.log_count || null, + logs: row.logs || [], + auth_user: row.auth_user || null, + } + }) + + // Just use the row timestamps directly for cursors + const lastRow = result.length > 0 ? result[result.length - 1] : null + const firstRow = result.length > 0 ? result[0] : null + const nextCursor = lastRow ? lastRow.timestamp : null + + // This ensures live mode never breaks the infinite query chain + // DataTableDemo uses milliseconds, but our timestamps are in microseconds + const prevCursor = result.length > 0 ? firstRow!.timestamp : new Date().getTime() * 1000 + + // HACK: Backend uses "timestamp > cursor" which can exclude records with identical timestamps + // THIS CAN SOMETIMES CAUSE 49 RECORDS INSTEAD OF 50 TO BE RETURNED + // TODO: Revisit this - ideally the backend should use composite cursors (timestamp+id) for proper pagination + // For now, we consider either 49 or 50 records as a "full page" to ensure pagination works correctly + const hasMore = result.length >= LOGS_PAGE_LIMIT - 1 + + return { + data: result, + prevCursor, + nextCursor: hasMore ? nextCursor : null, + } +} + +export const useUnifiedLogsInfiniteQuery = ( + { projectRef, search }: UnifiedLogsVariables, + { + enabled = true, + ...options + }: UseInfiniteQueryOptions = {} +) => { + return useInfiniteQuery( + logsKeys.unifiedLogsInfinite(projectRef, search), + ({ signal, pageParam }) => { + return getUnifiedLogs({ projectRef, search, pageParam }, signal) + }, + { + enabled: enabled && typeof projectRef !== 'undefined', + getNextPageParam(lastPage, pages) { + // Only return a cursor if we actually have more data to fetch + if (!lastPage.nextCursor || lastPage.data.length === 0) return null + // Only trigger fetch when specifically requested, not during column resizing + return { cursor: lastPage.nextCursor, direction: 'next' } as PageParam + }, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + refetchInterval: 0, + staleTime: UNIFIED_LOGS_STALE_TIME, + ...options, + } + ) +} diff --git a/apps/studio/data/projects/project-create-mutation.ts b/apps/studio/data/projects/project-create-mutation.ts index ceaefb80bb3eb..1e6e4c3e313fc 100644 --- a/apps/studio/data/projects/project-create-mutation.ts +++ b/apps/studio/data/projects/project-create-mutation.ts @@ -21,7 +21,7 @@ const WHITELIST_ERRORS = [ export type ProjectCreateVariables = { name: string - organizationId: number + organizationSlug: string dbPass: string dbRegion: string dbSql?: string @@ -38,7 +38,7 @@ export type ProjectCreateVariables = { export async function createProject({ name, - organizationId, + organizationSlug, dbPass, dbRegion, dbSql, @@ -53,7 +53,7 @@ export async function createProject({ }: ProjectCreateVariables) { const body: components['schemas']['CreateProjectBody'] = { cloud_provider: cloudProvider, - org_id: organizationId, + organization_slug: organizationSlug, name, db_pass: dbPass, db_region: dbRegion, diff --git a/apps/studio/hooks/analytics/useLogsPreview.tsx b/apps/studio/hooks/analytics/useLogsPreview.tsx index 1e6d5a69ef6d3..27a62b248f21b 100644 --- a/apps/studio/hooks/analytics/useLogsPreview.tsx +++ b/apps/studio/hooks/analytics/useLogsPreview.tsx @@ -1,6 +1,6 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' -import { useMemo, useState, useCallback } from 'react' +import { useCallback, useMemo, useState } from 'react' import { LogsTableName, @@ -22,8 +22,8 @@ import { genDefaultQuery, } from 'components/interfaces/Settings/Logs/Logs.utils' import { get } from 'data/fetchers' -import { useLogsUrlState } from './useLogsUrlState' import { useFillTimeseriesSorted } from './useFillTimeseriesSorted' +import { useLogsUrlState } from './useLogsUrlState' import useTimeseriesUnixToIso from './useTimeseriesUnixToIso' interface LogsPreviewHook { @@ -83,12 +83,11 @@ function useLogsPreview({ const params: LogsEndpointParams = useMemo(() => { const currentSql = genDefaultQuery(table, mergedFilters, limit) return { - project: projectRef, iso_timestamp_start: timestampStart, iso_timestamp_end: timestampEnd, sql: currentSql, } - }, [projectRef, timestampStart, timestampEnd, table, mergedFilters, limit]) + }, [timestampStart, timestampEnd, table, mergedFilters, limit]) const queryKey = useMemo(() => ['projects', projectRef, 'logs', params], [projectRef, params]) @@ -177,7 +176,6 @@ function useLogsPreview({ params: { path: { ref: projectRef }, query: { - project: projectRef, sql: countQuerySql, iso_timestamp_start: latestRefresh, iso_timestamp_end: timestampEnd, @@ -228,7 +226,6 @@ function useLogsPreview({ query: { iso_timestamp_start: timestampStart, iso_timestamp_end: timestampEnd, - project: projectRef, sql: chartQuery, }, }, diff --git a/apps/studio/hooks/analytics/useLogsQuery.tsx b/apps/studio/hooks/analytics/useLogsQuery.tsx index 77fec615d8813..84ed4c54d459f 100644 --- a/apps/studio/hooks/analytics/useLogsQuery.tsx +++ b/apps/studio/hooks/analytics/useLogsQuery.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query' -import { Dispatch, SetStateAction, useState, useEffect } from 'react' +import { Dispatch, SetStateAction, useEffect, useState } from 'react' import { IS_PLATFORM } from 'common' import { @@ -37,7 +37,6 @@ const useLogsQuery = ( const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS) const [params, setParams] = useState({ sql: initialParams?.sql || '', - project: projectRef, iso_timestamp_start: initialParams.iso_timestamp_start ? initialParams.iso_timestamp_start : defaultHelper.calcFrom(), diff --git a/apps/studio/hooks/analytics/useSingleLog.tsx b/apps/studio/hooks/analytics/useSingleLog.tsx index 291c17ecb47ee..6ca288c2122b8 100644 --- a/apps/studio/hooks/analytics/useSingleLog.tsx +++ b/apps/studio/hooks/analytics/useSingleLog.tsx @@ -31,7 +31,7 @@ function useSingleLog({ const table = queryType ? LOGS_TABLES[queryType] : undefined const sql = id && table ? genSingleLogQuery(table, id) : '' - const params: LogsEndpointParams = { ...paramsToMerge, project: projectRef, sql } + const params: LogsEndpointParams = { ...paramsToMerge, sql } const isWarehouseQuery = queryType === 'warehouse' // Warehouse queries are handled differently diff --git a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx index 5d3d5e2a23e1b..f656892e94e70 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx @@ -137,8 +137,8 @@ const CreateProject = () => { async function onCreateProject() { if (!organizationIntegration) return console.error('No organization installation details found') if (!organizationIntegration?.id) return console.error('No organization installation ID found') - if (!foreignProjectId) return console.error('No foreignProjectId ID set') - if (!organization) return console.error('No organization ID set') + if (!foreignProjectId) return console.error('No foreignProjectId set') + if (!organization) return console.error('No organization set') snapshot.setLoading(true) @@ -150,7 +150,7 @@ const CreateProject = () => { } createProject({ - organizationId: organization.id, + organizationSlug: organization.slug, name: projectName, dbPass, dbRegion, diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index ce537860dca93..2ba15120023c5 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -368,7 +368,7 @@ const Wizard: NextPageWithLayout = () => { const data: ProjectCreateVariables = { cloudProvider: cloudProvider, - organizationId: currentOrg.id, + organizationSlug: currentOrg.slug, name: projectName, dbPass: dbPass, dbRegion: dbRegion,